Avoid unnecessary copies

4

I have an object that internally stores a rather heavy collection of objects. This collection is filled on demand, that is, if the collection is not necessary it will not be filled in life:

class Objeto
{
  std::vector<int> coleccion;

  // Solo una factoría puede crear objetos de este tipo
  Objeto();

  void RellenarColeccion()
  { /* ... */ }

public:

  // No se admiten copias
  Objeto(Objeto const&) = delete;

  // Constructor move
  Objeto(Objeto &&) = default;

  std::vector<int> const& Coleccion()
  {
    if( coleccion.empty() )
      RellenarColeccion();

     return coleccion;
  }
};

The class works perfectly. By returning a reference to the collection copies are avoided and the program ends up working reasonably well ... until the need arises to copy the collection of the object.

This need arises in a particular situation in which the only thing that interests the object is its internal collection:

Objeto NuevoObjeto() // Función que emula la factoría
{ return Objeto; }

int main()
{
  // Se invoca la sintaxis move y la copia es bastante liviana
  Objeto o1 = NuevoObjeto();

  // Obtenemos la colección vía referencia. Evitamos la copia
  auto& v1 = o1.Coleccion();

  // El problema lo tenemos cuando del objeto solo queremos su coleccion
  // La copia es demasiado pesada
  auto v2 = NuevoObjeto().Coleccion();

  // Y no nos podemos quedar con la referencia
  auto& v3 = NuevoObjeto().Coleccion(); // Referencia a objeto destruido
}

Is there any solution that allows you to take advantage of the syntax move making changes only in Objeto ? What would be expected would be the following:

int main()
{
  // caso 1: se accede por referencia
  Objeto o1 = NuevoObjeto();
  auto& v1 = o1.Coleccion();

  // caso 2: Se llama a la sintaxis move para evitar la copia del vector
  auto v2 = NuevoObjeto().Coleccion();
}
    
asked by eferion 30.08.2017 в 13:26
source

2 answers

2

It is possible to mark a member function to be called when it is applied to a temporary object (or object of right-type type), for that reason && is added at the end of the signature of the function, this is known as " reference qualifiers " or " reference to right-value for *this ", if you use these functions at your Objeto :

std::vector<int> const& Coleccion() &
{
    if( coleccion.empty() )
        RellenarColeccion();

    return coleccion;
}

// Esta version de Coleccion sera llamada cuando el objeto sea un temporal
std::vector<int> Coleccion() &&
{
    if( coleccion.empty() )
        RellenarColeccion();

    return std::move(coleccion);
 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ <-- movemos la coleccion fuera del temporal
}

In the version that uses a reference to the right value for *this , we use std::move to transform the coleccion of the temporary object into a vector value-right, which will be used to construct the resulting std::vector<int> that, when receiving a right-value, it will move the content instead of copying it 1 . To see if this works as you need, I made some changes in your example class:

class Objeto
{
    using type_t = chivato;
    ::vector<type_t> coleccion;

    Objeto() { std::cout << this << ' ' << __FUNCTION__ << '\n'; }

    friend Objeto NuevoObjeto(); // Otorgamos acceso a los miembros privados para la factoria

    void RellenarColeccion()
    { coleccion.emplace_back(); } // Anyadimos un objeto como prueba

public:
    ~Objeto() { std::cout << this << ' ' << __FUNCTION__ << '\n'; } // mostramos mensaje al destruir

    Objeto(Objeto const&) = delete;
    Objeto(Objeto &&) = default;

    std::vector<type_t> const& Coleccion() &
    {
        std::cout << "No temporal\n";
        if( coleccion.empty() )
            RellenarColeccion();

        return coleccion;
    }


    std::vector<type_t> Coleccion() &&
    {
        std::cout << "Temporal\n";
        if( coleccion.empty() )
            RellenarColeccion();

        return std::move(coleccion);
    }
};

I added some messages in the construction and destruction of Objeto and changed the type of the object contained in the collection to this:

struct chivato
{
    chivato() { std::cout << this << ' ' << __FUNCTION__ << '\n'; }
    ~chivato() { std::cout << this << ' ' << __FUNCTION__ << '\n'; }
};

With these changes, if we execute this code in main :

std::cout << "Objeto en main\n";
Objeto o1 = NuevoObjeto();
std::cout << "Referencia a coleccion\n";
auto& v1 = o1.Coleccion();

std::cout << "\nObjeto temporal del que se obtiene coleccion\n";
auto v2 = NuevoObjeto().Coleccion();
std::cout << "Despues\n";

We have this exit 2 :

Objeto en main
0x000000000001 Objeto
Referencia a coleccion
No temporal
0x0000001 chivato

Objeto temporal del que se obtiene coleccion
0x000000000002 Objeto
Temporal
0x0000002 chivato
0x000000000002 ~Objeto
Despues
0x0000002 ~chivato
0x000000000001 ~Objeto
0x0000001 ~chivato

We see that the first Objeto ( 0x000000000001 ) creates a collection with the element 0x0000001 and that collection is read through the function Coleccion that does not apply over temporary, we do not see that the element 0x0000001 be destroyed.

In the second part a temporary Objeto ( 0x000000000002 ) is created that contains the 0x0000002 element and then the Objeto is destroyed without the element ( 0x0000002 ) being destroyed, that means that the collection has been moved, we can also see that it has been called the version of Coleccion that is called over temporary.

After the message Después are called the destructors of the collection element of Objeto created temporarily from the factory ( 0x0000002 ), then the destructor of the first Objeto ( 0x000000000001 ) created is called the which calls the destructor of its unique element ( 0x0000001 ).

This behavior shows us that there has been no copy of the collection obtained from the storm (if there had been a copy it would be an additional construction-destruction of collection elements).

You can see the code working In Wandbox 三 へ (へ ਊ) へ ハ ッ ハ ッ .

1 Attention, using std::move in a% instruction return avoids optimizing the return value .

2 I have edited the memory addresses, for clarity and because different executions are different values.

    
answered by 30.08.2017 / 15:32
source
0

Ok, what you have is the following:

  std::vector<int> const& Coleccion()
  {
    if( coleccion.empty() )
      RellenarColeccion();

     return coleccion;
  }

As you can see, there you return a list, so a copy will be made when creating v2. What you can do is return a pointer to that collection, so you will simply be passing an address instead of a complete list.

You could do it by returning a void * and then doing a casting to your std :: vector:

int *coleccion = (int*)coleccion; //siendo coleccion lo que te ha retornado Coleccion()
std::vector<int > v(coleccion, coleccion+ len); //len es el tamaño de tu std::vector original

The modification I propose for your Collection method () would look like this:

  void * const& Coleccion()
  {
    if( coleccion.empty() )
      RellenarColeccion();

     return (void*)&coleccion[0];  //retornamos la dirección de la primera posición
  }

So what you do is work with references and not with copies of data. I hope it serves you.

    
answered by 30.08.2017 в 13:42