c++ - semántica - sintaxis y semantica de un lenguaje de programacion ejemplo




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? (3)

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 objeto vector 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 usar void someFunction(const std::vector<std::string> & items) y nuestro m_items ya era lo suficientemente grande (en términos de capacidad) para acomodar los items , la asignación m_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:

Resumen:

Mientras no tengas tales prototipos, esta es una gran opción.


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





unique-ptr