c++ - semántica - sintaxis y semantica de los lenguajes
Para admitir la semántica de movimientos, ¿deberían tomarse los parámetros de la función por unique_ptr, por valor o por rvalue? (4)
A menos que tenga una razón para que el vector viva en el montón, recomendaría no utilizar unique_ptr
El almacenamiento interno del vector vive en el montón de todos modos, por lo que requerirá 2 grados de indirección si usa unique_ptr
, uno para eliminar el puntero del vector y nuevamente para eliminar la referencia del búfer de almacenamiento interno.
Como tal, aconsejaría utilizar 2 o 3.
Si opta por la opción 3 (que requiere una referencia de valor), está exigiendo a los usuarios de su clase que pasen un valor de valor (ya sea directamente de un temporal o se mueva de un valor), al llamar a someFunction
.
El requisito de pasar de un valor es oneroso.
Si sus usuarios desean mantener una copia del vector, tienen que saltar a través de aros para hacerlo.
std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));
Sin embargo, si elige la opción 2, el usuario puede decidir si desea conservar una copia o no, la elección es suya.
Guarde una copia:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
No guarde una copia:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don''t keep a copy
Una de mis funciones toma un vector como parámetro y lo almacena como una variable miembro. Estoy utilizando la referencia constante a un vector como se describe a continuación.
class Test {
public:
void someFunction(const std::vector<string>& items) {
m_items = items;
}
private:
std::vector<string> m_items;
};
Sin embargo, a veces los items
contienen un gran número de cadenas, por lo que me gustaría agregar una función (o reemplazar la función por una nueva) que admita la semántica de movimientos.
Estoy pensando en varios enfoques, pero no estoy seguro de cuál elegir.
1) unique_ptr
void someFunction(std::unique_ptr<std::vector<string>> items) {
// Also, make `m_itmes` std::unique_ptr<std::vector<string>>
m_items = std::move(items);
}
2) pasar por valor y mover
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
3) valor
void someFunction(std::vector<string>&& items) {
m_items = std::move(items);
}
¿Qué enfoque debo evitar y por qué?
Depende de tus patrones de uso:
Opción 1
Pros:
- La responsabilidad se expresa explícitamente y se pasa de la persona que llama a la persona que llama
Contras:
- A menos que el vector ya estuviera envuelto con
unique_ptr
, esto no mejora la legibilidad - Los punteros inteligentes en general administran objetos asignados dinámicamente. Por lo tanto, su
vector
debe convertirse en uno. Dado que los contenedores de bibliotecas estándar son objetos administrados que utilizan asignaciones internas para el almacenamiento de sus valores, esto significa que habrá dos asignaciones dinámicas para cada vector. Uno para el bloque de administración de la ptr única + el objetovector
sí mismo y uno adicional para los elementos almacenados.
Resumen:
Si administras este vector de forma unique_ptr
con unique_ptr
, sigue usándolo, de lo contrario no lo harás.
opcion 2
Pros:
Esta opción es muy flexible, ya que permite a la persona que llama decidir si desea conservar una copia o no:
std::vector<std::string> vec { ... }; Test t; t.someFunction(vec); // vec stays a valid copy t.someFunction(std::move(vec)); // vec is moved
Cuando la persona que llama usa
std::move()
el objeto solo se mueve dos veces (sin copias), lo cual es eficiente.
Contras:
- Cuando la persona que llama no usa
std::move()
, siempre se llama a un constructor de copia para crear el objeto temporal. Si tuviéramos que usarvoid someFunction(const std::vector<std::string> & items)
y nuestrom_items
ya era lo suficientemente grande (en términos de capacidad) para acomodar lositems
, la asignaciónm_items = items
habría sido solo una copia Operación, sin la asignación extra.
Resumen:
Si sabe de antemano que este objeto se reiniciará muchas veces durante el tiempo de ejecución, y la persona que llama no siempre usa std::move()
, lo habría evitado. De lo contrario, esta es una gran opción, ya que es muy flexible, ya que permite la facilidad de uso y un mayor rendimiento por demanda a pesar del escenario problemático.
Opcion 3
Contras:
Esta opción obliga a la persona que llama a renunciar a su copia. Entonces, si quiere guardar una copia para sí mismo, debe escribir un código adicional:
std::vector<std::string> vec { ... }; Test t; t.someFunction(std::vector<std::string>{vec});
Resumen:
Esto es menos flexible que la Opción # 2 y, por lo tanto, diría que es inferior en la mayoría de los escenarios.
Opcion 4
Dados los contras de las opciones 2 y 3, consideraría sugerir una opción adicional:
void someFunction(const std::vector<int>& items) {
m_items = items;
}
// AND
void someFunction(std::vector<int>&& items) {
m_items = std::move(items);
}
Pros:
- Resuelve todos los escenarios problemáticos descritos para las opciones 2 y 3 mientras disfruta de sus ventajas.
- La persona que llama decidió guardar una copia para sí mismo o no
- Se puede optimizar para cualquier escenario dado
Contras:
- Si el método acepta muchos parámetros como referencias const o rvalue, el número de prototipos crece exponencialmente
Resumen:
Mientras no tengas tales prototipos, esta es una gran opción.
El consejo actual sobre esto es tomar el vector por valor y moverlo a la variable miembro:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
Y acabo de marcar, std::vector
proporciona un operador de asignación de movimiento. Si la persona que llama no quiere guardar una copia, puede moverla a la función en el sitio de la llamada: fn(std::move(vec));
.
En la superficie, la opción 2 parece una buena idea, ya que maneja lvalues y rvalues en una sola función. Sin embargo, como señala Herb Sutter en su CppCon 2014, ¡ vuelve a lo básico! Elementos esenciales del estilo moderno de C ++ , esto es una pesimista para el caso común de los valores l.
Si m_items
era "más grande" que los items
, su código original no asignará memoria para el vector:
// Original code:
void someFunction(const std::vector<string>& items) {
// If m_items.capacity() >= items.capacity(),
// there is no allocation.
// Copying the strings may still require
// allocations
m_items = items;
}
El operador de asignación de copia en std::vector
es lo suficientemente inteligente como para reutilizar la asignación existente. Por otro lado, tomar el parámetro por valor siempre tendrá que hacer otra asignación:
// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
En pocas palabras: la construcción de copias y la asignación de copias no tienen necesariamente el mismo costo. No es improbable que la asignación de copias sea más eficiente que la construcción de copias, es más eficiente para std::vector
y std::string
† .
La solución más fácil, como señala Herb, es agregar una sobrecarga de valor (básicamente su opción 3):
// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
m_items = std::move(items);
}
Tenga en cuenta que la optimización de la asignación de copias solo funciona cuando ya existen m_items
, por lo que llevar los parámetros a los constructores por valor es totalmente correcto: la asignación debería realizarse de cualquier manera.
TL; DR: elija agregar la opción 3. Es decir, tenga una sobrecarga para los valores de l y otra para los valores de r. La opción 2 fuerza la construcción de la copia en lugar de la asignación de copia, que puede ser más costosa (y es para std::string
y std::vector
)
† Si desea ver puntos de referencia que muestren que la opción 2 puede ser una pesimista, en este punto de la charla , Herb muestra algunos puntos de referencia
‡ No deberíamos haber marcado esto como no noexcept
si el operador de asignación de movimientos de std::vector
no era noexcept
. Consulte la documentación si está utilizando un asignador personalizado.
Como regla general, tenga en cuenta que las funciones similares solo deben marcarse como no noexcept
si la asignación de movimiento del tipo es noexcept