Como @ JDługosz señala en los comentarios, Herb da otros consejos en otra (¿más tarde?) Charla, ver más o menos desde aquí: https://youtu.be/xnqTKD8uD64?t=54m50s .
Su consejo se reduce a usar solo parámetros de valor para una función f
que toma los llamados argumentos de sumidero, suponiendo que moverá la construcción de estos argumentos de sumidero.
Este enfoque general solo agrega la sobrecarga de un constructor de movimiento para los argumentos lvalue y rvalue en comparación con una implementación óptima de f
argumentos adaptados a lvalue y rvalue respectivamente. Para ver por qué este es el caso, supongamos que f
toma un parámetro de valor, donde T
hay algún tipo constructivo de copiar y mover:
void f(T x) {
T y{std::move(x)};
}
Llamar f
con un argumento lvalue dará como resultado que se llame a un constructor de copia para construir x
, y que se llame a un constructor de movimiento para construir y
. Por otro lado, llamar f
con un argumento rvalue hará que se llame a un constructor de movimiento para construir x
, y que se llame a otro constructor de movimiento para construir y
.
En general, la implementación óptima de los f
argumentos de for lvalue es la siguiente:
void f(const T& x) {
T y{x};
}
En este caso, solo se llama a un constructor de copia para construir y
. La implementación óptima de los f
argumentos de rvalue es, de nuevo en general, como sigue:
void f(T&& x) {
T y{std::move(x)};
}
En este caso, solo se llama a un constructor de movimiento para construir y
.
Por lo tanto, un compromiso razonable es tomar un parámetro de valor y hacer que un constructor de movimiento adicional llame a los argumentos lvalue o rvalue con respecto a la implementación óptima, que también es el consejo dado en la charla de Herb.
Como @ JDługosz señaló en los comentarios, pasar por valor solo tiene sentido para las funciones que construirán algún objeto a partir del argumento sumidero. Cuando tiene una función f
que copia su argumento, el enfoque de paso por valor tendrá más gastos generales que un enfoque de paso por referencia general. El enfoque de paso por valor para una función f
que retiene una copia de su parámetro tendrá la forma:
void f(T x) {
T y{...};
...
y = std::move(x);
}
En este caso, hay una construcción de copia y una asignación de movimiento para un argumento lvalue, y una construcción de movimiento y una asignación de movimiento para un argumento rvalue. El caso más óptimo para un argumento lvalue es:
void f(const T& x) {
T y{...};
...
y = x;
}
Esto se reduce a una asignación solamente, que es potencialmente mucho más barata que el constructor de copia más la asignación de movimiento requerida para el enfoque de paso por valor. La razón de esto es que la asignación podría reutilizar la memoria asignada existente y
y, por lo tanto, evitar (des) asignaciones, mientras que el constructor de copias generalmente asignará memoria.
Para un argumento rvalue, la implementación más óptima para f
que retiene una copia tiene la forma:
void f(T&& x) {
T y{...};
...
y = std::move(x);
}
Entonces, solo una asignación de movimiento en este caso. Pasar un valor r a la versión de f
eso toma una referencia constante solo cuesta una asignación en lugar de una asignación de movimiento. Hablando relativamente, la versión de f
tomar una referencia constante en este caso como la implementación general es preferible.
Por lo tanto, en general, para la implementación más óptima, deberá sobrecargar o realizar algún tipo de reenvío perfecto como se muestra en la charla. El inconveniente es una explosión combinatoria en el número de sobrecargas requeridas, dependiendo del número de parámetros f
en caso de que opte por sobrecargar en la categoría de valor del argumento. El reenvío perfecto tiene el inconveniente de que se f
convierte en una función de plantilla, lo que evita que sea virtual, y da como resultado un código significativamente más complejo si desea obtener el 100% correcto (consulte la charla para obtener los detalles sangrientos).