Como @ JDługosz aponta nos comentários, Herb dá outros conselhos em outra conversa (mais tarde?), Veja mais ou menos daqui: https://youtu.be/xnqTKD8uD64?t=54m50s .
Seu conselho se resume a usar apenas parâmetros de valor para uma função f
que recebe os chamados argumentos de afundamento, supondo que você mova a construção desses argumentos de afundamento.
Essa abordagem geral apenas adiciona a sobrecarga de um construtor de movimentação para os argumentos lvalue e rvalue, em comparação com uma implementação ideal de f
argumentos personalizados para lvalue e rvalue, respectivamente. Para ver por que esse é o caso, suponha que f
use um parâmetro value, onde T
estão alguns tipos construtíveis de copiar e mover:
void f(T x) {
T y{std::move(x)};
}
Chamar f
com um argumento lvalue resultará em um construtor de cópia sendo chamado para construir x
e um construtor de movimentação sendo chamado para construir y
. Por outro lado, chamar f
com um argumento rvalue fará com que um construtor de movimentação seja chamado para construir x
e outro construtor de movimentação seja chamado para construção y
.
Em geral, a implementação ideal dos f
argumentos for lvalue é a seguinte:
void f(const T& x) {
T y{x};
}
Nesse caso, apenas um construtor de cópia é chamado para construir y
. A implementação ideal dos f
argumentos for rvalue é, novamente em geral, da seguinte maneira:
void f(T&& x) {
T y{std::move(x)};
}
Nesse caso, apenas um construtor de movimentação é chamado para construir y
.
Portanto, um compromisso sensato é pegar um parâmetro de valor e solicitar um construtor de movimento extra para argumentos lvalue ou rvalue com relação à implementação ideal, que também é o conselho dado na palestra de Herb.
Como @ JDługosz apontou nos comentários, passar por valor só faz sentido para funções que construirão algum objeto a partir do argumento coletor. Quando você tem uma função f
que copia seu argumento, a abordagem de passagem por valor terá mais sobrecarga do que uma abordagem geral de referência de passagem por const. A abordagem de passagem por valor de uma função f
que retém uma cópia de seu parâmetro terá a seguinte forma:
void f(T x) {
T y{...};
...
y = std::move(x);
}
Nesse caso, há uma construção de cópia e uma atribuição de movimentação para um argumento lvalue, e uma construção de movimentação e atribuição de movimentação para um argumento rvalue. O caso mais ideal para um argumento lvalue é:
void f(const T& x) {
T y{...};
...
y = x;
}
Isso se resume apenas a uma atribuição, que é potencialmente muito mais barata que o construtor de cópias, além da atribuição de movimentação necessária para a abordagem de passagem por valor. A razão para isso é que a atribuição pode reutilizar a memória alocada existente y
e, portanto, impedir (des) as alocações, enquanto o construtor de cópias geralmente aloca memória.
Para um argumento rvalue, a implementação mais ideal para f
reter uma cópia tem o formato:
void f(T&& x) {
T y{...};
...
y = std::move(x);
}
Portanto, apenas uma atribuição de movimentação neste caso. Passar um rvalor para a versão f
que leva uma referência const apenas custa uma atribuição em vez de uma atribuição de movimentação. Então, relativamente falando, a versão de f
tomar uma referência const neste caso, como a implementação geral, é preferível.
Portanto, em geral, para a implementação ideal, você precisará sobrecarregar ou fazer algum tipo de encaminhamento perfeito, como mostrado na palestra. A desvantagem é uma explosão combinatória no número de sobrecargas necessárias, dependendo do número de parâmetros f
, caso você opte por sobrecarregar a categoria de valor do argumento. O encaminhamento perfeito tem a desvantagem que f
se torna uma função de modelo, o que evita torná-lo virtual, e resulta em um código significativamente mais complexo, se você deseja obtê-lo 100% da maneira correta (consulte a palestra para obter detalhes sangrentos).