Estoy aprendiendo C ++ en este momento y trato de evitar adquirir malos hábitos. Por lo que tengo entendido, clang-tidy contiene muchas "mejores prácticas" y trato de ceñirme a ellas lo mejor posible (aunque no necesariamente entiendo por qué se consideran buenas todavía), pero no estoy seguro de si entienda lo que se recomienda aquí.
Usé esta clase del tutorial:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
Esto lleva a una sugerencia de clang-tidy de que debería pasar por valor en lugar de referencia y uso std::move
. Si lo hago, recibo la sugerencia de hacer name
una referencia (para asegurarme de que no se copien todas las veces) y la advertencia que std::move
no tendrá ningún efecto porque name
es const
así que debería eliminarla.
La única forma en que no recibo una advertencia es eliminando por const
completo:
Creature(std::string name)
: m_name{std::move(name)}
{
}
Lo que parece lógico, ya que el único beneficio de const
era evitar jugar con la cadena original (lo que no sucede porque pasé por valor). Pero leí en CPlusPlus.com :
Aunque tenga en cuenta que, en la biblioteca estándar, mover implica que el objeto movido desde se deja en un estado válido pero no especificado. Lo que significa que, después de una operación de este tipo, el valor del objeto desde el que se movió solo debe destruirse o asignarse un nuevo valor; acceder a él de otro modo produce un valor no especificado.
Ahora imagina este código:
std::string nameString("Alex");
Creature c(nameString);
Debido a que nameString
se pasa por valor, std::move
solo se invalidará name
dentro del constructor y no tocará la cadena original. Pero, ¿cuáles son las ventajas de esto? Parece que el contenido se copia solo una vez de todos modos: si paso por referencia cuando llamo m_name{name}
, si paso por valor cuando lo paso (y luego se mueve). Entiendo que esto es mejor que pasar por valor y no usar std::move
(porque se copia dos veces).
Entonces dos preguntas:
- ¿Entendí correctamente lo que está pasando aquí?
- ¿Hay alguna ventaja de usar
std::move
sobrepasar por referencia y simplemente llamarm_name{name}
?
Creature c("John");
hace una copia adicionalstd::string_view
y el SSO.clang-tidy
es una excelente manera de obsesionarme con microoptimizaciones innecesarias a expensas de la legibilidad. La pregunta que debemos hacer aquí, antes que nada, es cuántas veces llamamos realmente alCreature
constructor.Respuestas:
Si.
Una firma de función fácil de entender sin sobrecargas adicionales. La firma revela inmediatamente que se copiará el argumento; esto evita que las personas que llaman se pregunten si una
const std::string&
referencia podría almacenarse como un miembro de datos, lo que posiblemente se convierta en una referencia pendiente más adelante. Y no hay necesidad de sobrecargar argumentosstd::string&& name
yconst std::string&
para evitar copias innecesarias cuando se pasan rvalues a la función. Pasando un lvaluea la función que toma su argumento por valor causa una copia y una construcción de movimiento. Pasando un rvalue a la misma función
provoca dos construcciones de movimiento. Por el contrario, cuando el parámetro de la función es
const std::string&
, siempre habrá una copia, incluso al pasar un argumento rvalue. Esto es claramente una ventaja siempre que el tipo de argumento sea barato de mover-construir (este es el casostd::string
).Pero hay una desventaja a considerar: el razonamiento no funciona para funciones que asignan el argumento de la función a otra variable (en lugar de inicializarlo):
provocará una desasignación del recurso al que se
m_name
refiere antes de que se reasigne. Recomiendo leer el ítem 41 en Effective Modern C ++ y también esta pregunta .fuente
move
, el espacio se desasignará. Si no lo usomove
, solo se desasignará si el espacio asignado es demasiado pequeño para contener la nueva cadena, lo que mejora el rendimiento. ¿Es eso correcto?m_name
desde unconst std::string&
parámetro, la memoria interna se reutiliza siempre quem_name
encaje. Al asignar un movimientom_name
, la memoria debe desasignarse de antemano. De lo contrario, era imposible "robar" los recursos del lado derecho de la tarea.Un lvalue pasado se une a
name
y luego se copia enm_name
.Un rvalue pasado se une a
name
y luego se copia enm_name
.Un pasado lvalue se copian en
name
, a continuación, se trasladó am_name
.Un rvalue pasado se mueve a
name
, luego se mueve am_name
.Un lvalue pasado se une a
name
y luego se copia enm_name
.Un rvalue pasado se une a
rname
y luego se mueve am_name
.Como las operaciones de movimiento suelen ser más rápidas que las copias, (1) es mejor que (0) si pasa muchos temporales. (2) es óptimo en términos de copias / movimientos, pero requiere repetición de código.
La repetición del código se puede evitar con un reenvío perfecto :
Opcionalmente, puede querer restringir
T
para restringir el dominio de tipos con los que se puede crear una instancia de este constructor (como se muestra arriba). C ++ 20 tiene como objetivo simplificar esto con Concepts .En C ++ 17, los valores prvalues se ven afectados por la elisión de copia garantizada , que, cuando sea aplicable, reducirá el número de copias / movimientos al pasar argumentos a funciones.
fuente
Creature(const std::string &name) : m_name{std::move(name)} { }
en el (2) ?La forma en que aprueba no es la única variable aquí, lo que aprueba marca la gran diferencia entre los dos.
En C ++, tenemos todo tipo de categorías de valor y exista este "lenguaje" para los casos en que se pasa en un valor de lado derecho (como
"Alex-string-literal-that-constructs-temporary-std::string"
ostd::move(nameString)
), lo que da como resultado 0 copias destd::string
haber sido hecho (el tipo no siquiera tiene que ser copia Urbanizable para argumentos rvalue), y solo usastd::string
el constructor move.Preguntas y respuestas algo relacionadas .
fuente
Hay varias desventajas del enfoque de paso por valor y movimiento sobre la referencia de paso por (rv):
fuente
m_name{name}
parte donde se copia?std::string nameString("Alex"); Creature c(nameString);
un objeto esnameString
, otro es un argumento de función y el tercero es un campo de clase.En mi caso, cambiar para pasar por valor y luego hacer un std: move causó un error de uso de pila después de libre en Address Sanitizer.
https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165
Entonces, lo apagué, así como la sugerencia en clang-tidy.
https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332
fuente