Manera eficiente de devolver un std :: vector en c ++

106

Cuántos datos se copian, cuando se devuelve un std :: vector en una función y qué tan grande será la optimización para colocar el std :: vector en free-store (en el montón) y devolver un puntero en su lugar, es decir, es:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

más eficiente que:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

?

Morten
fuente
3
¿Qué tal pasar el vector por referencia y luego llenarlo por dentro f?
Kiril Kirov
4
RVO es una optimización bastante básica que la mayoría de los compiladores podrán realizar en cualquier momento.
Remus Rusanu
A medida que las respuestas fluyen, puede ayudarlo a aclarar si está utilizando C ++ 03 o C ++ 11. Las mejores prácticas entre las dos versiones varían bastante.
Drew Dormann
@Kiril Kirov, ¿Puedo hacer eso sin ponerlo en la lista de argumentos de la función? void f (std :: vector & result)?
Morten

Respuestas:

140

En C ++ 11, esta es la forma preferida:

std::vector<X> f();

Es decir, retorno por valor.

Con C ++ 11, std::vectortiene movimiento semántica, lo que significa el local de vector declarado en su función se trasladó a la entrega y en algunos casos incluso el movimiento puede ser elidido por el compilador.

Nawaz
fuente
13
@LeonidVolnitsky: Sí, si es local . De hecho, return std::move(v);desactivará la elisión de movimiento incluso si fue posible con solo return v;. Por lo que se prefiere este último.
Nawaz
1
@juanchopanza: No lo creo. Antes de C ++ 11, se podía argumentar en contra porque el vector no se movería; ¡y RVO depende del compilador! Habla sobre las cosas de los 80 y los 90.
Nawaz
2
Mi comprensión sobre el valor de retorno (por valor) es: en lugar de 'movido', el valor de retorno en el destinatario de la llamada se crea en la pila de la persona que llama, por lo que todas las operaciones en el destinatario de la llamada están en su lugar, no hay nada que mover en RVO . ¿Es eso correcto?
2018
2
@ r0ng: Sí, eso es cierto. Así es como los compiladores suelen implementar RVO.
Nawaz
1
@Nawaz No lo es. Ya no hay ni un movimiento.
Lightness Races in Orbit
70

Debería devolver por valor.

El estándar tiene una característica específica para mejorar la eficiencia de la devolución por valor. Se llama "copia elisión", y más específicamente en este caso la "optimización del valor de retorno con nombre (NRVO)".

Los compiladores no tienen que implementarlo, pero de nuevo los compiladores no tienen que implementar la función de alineación (o realizar ninguna optimización). Pero el rendimiento de las bibliotecas estándar puede ser bastante pobre si los compiladores no optimizan, y todos los compiladores serios implementan inlining y NRVO (y otras optimizaciones).

Cuando se aplica NRVO, no habrá copia en el siguiente código:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

Pero el usuario puede querer hacer esto:

std::vector<int> myvec;
... some time later ...
myvec = f();

Copiar la elisión no impide una copia aquí porque es una asignación en lugar de una inicialización. Sin embargo, aún debe devolver por valor. En C ++ 11, la asignación se optimiza mediante algo diferente, llamado "semántica de movimiento". En C ++ 03, el código anterior causa una copia, y aunque en teoría un optimizador podría evitarlo, en la práctica es demasiado difícil. Entonces myvec = f(), en lugar de , en C ++ 03 debes escribir esto:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

Existe otra opción, que es ofrecer una interfaz más flexible al usuario:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

Luego, también puede admitir la interfaz existente basada en vectores además de eso:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

Esto podría ser menos eficiente que su código existente, si su código existente se usa reserve()de una manera más compleja que solo una cantidad fija por adelantado. Pero si su código existente básicamente llama push_backal vector repetidamente, entonces este código basado en plantillas debería ser tan bueno.

Steve Jessop
fuente
Votó a favor la respuesta realmente mejor y detallada. Sin embargo, en su variante swap () ( para C ++ 03 sin NRVO ) todavía tendrá una copia del constructor de copia hecha dentro de f (): desde el resultado de la variable a un objeto temporal oculto que finalmente se cambiará a myvec .
JenyaKh
@JenyaKh: claro, ese es un problema de calidad de implementación. El estándar no requería que las implementaciones de C ++ 03 implementaran NRVO, al igual que no requería la inserción de funciones. La diferencia con la inserción de funciones es que la inserción no cambia la semántica o su programa, mientras que NRVO sí lo hace. El código portátil debe funcionar con o sin NRVO. El código optimizado para una implementación particular (y banderas de compilador particulares) puede buscar garantías con respecto a NRVO en la propia documentación de la implementación.
Steve Jessop
3

Es hora de que publique una respuesta sobre RVO , yo también ...

Si devuelve un objeto por valor, el compilador a menudo lo optimiza para que no se construya dos veces, ya que es superfluo construirlo en la función como temporal y luego copiarlo. Esto se denomina optimización del valor de retorno: el objeto creado se moverá en lugar de copiarse.


fuente
1

Un modismo común anterior a C ++ 11 es pasar una referencia al objeto que se está completando.

Entonces no hay copia del vector.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 
Drew Dormann
fuente
3
Eso ya no es un modismo en C ++ 11.
Nawaz
1
@Nawaz Estoy de acuerdo. No estoy seguro de cuál es la mejor práctica ahora en SO con respecto a preguntas sobre C ++ pero no específicamente C ++ 11. Sospecho que debería estar inclinado a darle respuestas de C ++ 11 a un estudiante, respuestas de C ++ 03 a alguien que esté metido hasta la cintura en el código de producción. ¿Tienes una opinión?
Drew Dormann
7
En realidad, después del lanzamiento de C ++ 11 (que tiene 19 meses), considero que cada pregunta es una pregunta de C ++ 11, a menos que se indique explícitamente que es una pregunta de C ++ 03.
Nawaz
1

Si el compilador admite la optimización de valor devuelto con nombre ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), puede devolver directamente el vector siempre que no haya:

  1. Diferentes rutas que devuelven diferentes objetos con nombre
  2. Varias rutas de retorno (incluso si se devuelve el mismo objeto con nombre en todas las rutas) con estados EH introducidos.
  3. El objeto con nombre devuelto se referencia en un bloque asm en línea.

NRVO optimiza las llamadas redundantes al constructor y destructor de copias y, por lo tanto, mejora el rendimiento general.

No debería haber una diferencia real en tu ejemplo.

taocp
fuente
0
vector<string> getseq(char * db_file)

Y si desea imprimirlo en main (), debe hacerlo en un bucle.

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}
Akash Kandpal
fuente
-2

Por más agradable que pueda ser el "retorno por valor", es el tipo de código que puede llevar a uno a error. Considere el siguiente programa:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • P: ¿Qué pasará cuando se ejecute lo anterior? R: Un volcado de núcleo.
  • P: ¿Por qué el compilador no detectó el error? R: Porque el programa es sintácticamente, aunque no semánticamente, correcto.
  • P: ¿Qué sucede si modifica vecFunc () para devolver una referencia? R: El programa se ejecuta hasta su finalización y produce el resultado esperado.
  • P: ¿Cuál es la diferencia? R: El compilador no tiene que crear ni administrar objetos anónimos. El programador ha indicado al compilador que use exactamente un objeto para el iterador y para la determinación del punto final, en lugar de dos objetos diferentes como lo hace el ejemplo roto.

El programa erróneo anterior indicará que no hay errores incluso si se utilizan las opciones de informes GNU g ++ -Wall -Wextra -Weffc ++

Si debe producir un valor, lo siguiente funcionaría en lugar de llamar a vecFunc () dos veces:

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

Lo anterior tampoco produce objetos anónimos durante la iteración del bucle, pero requiere una posible operación de copia (que, como algunos señalan, podría optimizarse en algunas circunstancias. Pero el método de referencia garantiza que no se producirá ninguna copia. Creer que el compilador realizar RVO no sustituye a intentar construir el código más eficiente que puedas. Si puedes discutir la necesidad de que el compilador haga RVO, estás por delante del juego.

unclesmrgol dragón
fuente
3
Este es más un ejemplo de lo que puede salir mal si un usuario no está familiarizado con C ++ en general. Alguien que esté familiarizado con lenguajes basados ​​en objetos como .net o javascript probablemente asumirá que el vector de cadena siempre se pasa como un puntero y, por lo tanto, en su ejemplo siempre apuntaría al mismo objeto. vecfunc (). begin () y vecfunc (). end () no coincidirán necesariamente en su ejemplo, ya que deberían ser copias del vector de cadena.
Medran
-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Amruth A
fuente