¿Cómo evitar la copia al encadenar?

10

Estoy creando una clase de tipo encadenamiento, como el pequeño ejemplo a continuación. Parece que al encadenar funciones miembro, se invoca el constructor de copia. ¿Hay alguna forma de deshacerse de la llamada al constructor de la copia? En mi ejemplo de juguete a continuación, es obvio que solo estoy tratando con temporales y, por lo tanto, "debería" (tal vez no según los estándares, pero lógicamente) ser una elisión. La segunda mejor opción, para copiar elisión, sería llamar al constructor del movimiento, pero este no es el caso.

class test_class {
    private:
    int i = 5;
    public:
    test_class(int i) : i(i) {}
    test_class(const test_class& t) {
        i = t.i;
        std::cout << "Copy constructor"<< std::endl;
    }
    test_class(test_class&& t) {
        i = t.i;
        std::cout << "Move constructor"<< std::endl;
    }
    auto& increment(){
        i++;
        return *this;
    }
};
int main()
{
    //test_class a{7};
    //does not call copy constructor
    auto b = test_class{7};
    //calls copy constructor
    auto b2 = test_class{7}.increment();
    return 0;
}

Editar: algunas aclaraciones. 1. Esto no depende del nivel de optimización. 2. En mi código real, tengo objetos más complejos (p. Ej., Montón asignado) que ints

DDaniel
fuente
¿Qué nivel de optimización utiliza para compilar?
JVApen
2
auto b = test_class{7};no llama al constructor de copias porque es realmente equivalente test_class b{7};y los compiladores son lo suficientemente inteligentes como para reconocer este caso y, por lo tanto, pueden eludir fácilmente cualquier copia. No se puede hacer lo mismo b2.
Algún tipo programador el
En el ejemplo que se muestra, puede que no haya una diferencia real entre mover y copiar, y no todo el mundo lo sabe. Si pegas algo como un gran vector allí, podría ser un asunto diferente. Normalmente, mover solo tiene sentido para los tipos que usan recursos (como usar una gran cantidad de memoria de almacenamiento dinámico, etc.). ¿Es ese el caso aquí?
Darune el
El ejemplo parece artificial. ¿Realmente tiene E / S ( std::cout) en su copiadora? Sin ella, la copia debería optimizarse.
rustyx
@rustyx, elimine std :: cout y haga explícito el constructor de la copia. Esto demuestra que la copia de la elisión no depende de std :: cout.
DDaniel el

Respuestas:

7
  1. Respuesta parcial (no se construye b2en su lugar, pero convierte la construcción de la copia en una construcción de movimiento): puede sobrecargar la incrementfunción miembro en la categoría de valor de la instancia asociada:

    auto& increment() & {
        i++;
        return *this;
    }
    
    auto&& increment() && {
        i++;
       return std::move(*this);
    }

    Esto causa

    auto b2 = test_class{7}.increment();

    mover-construir b2porque test_class{7}es temporal, y se llama a la &&sobrecarga de test_class::increment.

  2. Para una verdadera construcción en el lugar (es decir, ni siquiera una construcción de movimiento), puede convertir todas las funciones de miembros especiales y no especiales en constexprversiones. Entonces puedes hacer

    constexpr auto b2 = test_class{7}.increment();

    y no tiene que pagar una mudanza ni una copia de construcción. Obviamente, esto es posible para el test_classescenario simple , pero no para un escenario más general que no permita constexprfunciones miembro.

lubgr
fuente
La segunda alternativa también hace que b2no sea modificable.
Algún tipo programador el
1
@Someprogrammerdude Buen punto. Supongo constinitque sería el camino a seguir allí.
lubgr
¿Cuál es el propósito de los símbolos después del nombre de la función? Nunca he visto eso antes.
Philip Nelson el
1
@PhilipNelson son calificadores de referencia y pueden dictar lo thismismo que las constfunciones de los miembros
kmdreko
1

Básicamente, asignar una a un requiere invocar un constructor, es decir, una copia o un movimiento . Esto es diferente de la donde se sabe que en ambos lados de la función es el mismo objeto distinto. También una puede referirse a un objeto compartido como un puntero.


Probablemente, la forma más simple es hacer que el constructor de copias esté completamente optimizado. El valor ya está optimizado por el compilador, es solo el std::coutque no se puede optimizar.

test_class(const test_class& t) = default;

(o simplemente elimine la copia y mueva el constructor)

ejemplo en vivo


Dado que su problema es básicamente con la referencia, una solución probablemente no sea devolver una referencia al objeto si desea dejar de copiar de esta manera.

  void increment();
};

auto b = test_class{7};//does not call copy constructor
b.increment();//does not call copy constructor

Un tercer método es confiar en la elisión de copia en primer lugar; sin embargo, esto requiere una reescritura o encapsulación de la operación en una función y, por lo tanto, evitar el problema por completo (soy consciente de que esto puede no ser lo que desea, pero podría ser un solución a otros usuarios):

auto b2 = []{test_class tmp{7}; tmp.increment().increment().increment(); return tmp;}(); //<-- b2 becomes 10 - copy constructor not called

Un cuarto método está utilizando un movimiento en su lugar, ya sea invocado explícitamente

auto b2 = std::move(test_class{7}.increment());

o como se ve en esta respuesta .

Darune
fuente
@ O'Neil estaba pensando en otro caso - ty, corregido
darune el