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 fque 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 fargumentos personalizados para lvalue e rvalue, respectivamente. Para ver por que esse é o caso, suponha que fuse um parâmetro value, onde Testão alguns tipos construtíveis de copiar e mover:
void f(T x) {
T y{std::move(x)};
}
Chamar fcom um argumento lvalue resultará em um construtor de cópia sendo chamado para construir xe um construtor de movimentação sendo chamado para construir y. Por outro lado, chamar fcom um argumento rvalue fará com que um construtor de movimentação seja chamado para construir xe outro construtor de movimentação seja chamado para construção y.
Em geral, a implementação ideal dos fargumentos 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 fargumentos 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 fque 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 fque 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 ye, 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 freter 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 fque 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 ftomar 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 fse 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).