¿Se acabaron los días en los que se pasaba const std::string & como parámetro?

Resuelto Benj asked hace 12 años • 13 respuestas

Escuché una charla reciente de Herb Sutter quien sugirió que las razones para pasar std::vectory std::stringpasar const &desaparecieron en gran medida. Sugirió que ahora es preferible escribir una función como la siguiente:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Entiendo que return_valserá un valor r en el punto en que la función regresa y, por lo tanto, se puede devolver usando la semántica de movimiento, que es muy económica. Sin embargo, invalsigue siendo mucho mayor que el tamaño de una referencia (que normalmente se implementa como un puntero). Esto se debe a que a std::stringtiene varios componentes, incluido un puntero al montón y un miembro char[]para la optimización de cadenas cortas. Entonces me parece que pasar por referencia sigue siendo una buena idea.

¿Alguien puede explicar por qué Herb podría haber dicho esto?

Benj avatar Apr 19 '12 22:04 Benj
Aceptado

La razón por la que Herb dijo lo que dijo es por casos como este.

Digamos que tengo una función Aque llama a una función B, que llama a una función C. Y Apasa una cuerda a través By dentro C. Ano sabe ni le importa C; todo Alo que sabe es B. Es decir, Ces un detalle de implementación de B.

Digamos que A se define de la siguiente manera:

void A()
{
  B("value");
}

Si B y C toman la cadena por const&, entonces se parece a esto:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Todo muy bien. Sólo estás pasando consejos, sin copiar, sin moverte, todos están contentos. Ctoma a const&porque no almacena la cadena. Simplemente lo usa.

Ahora quiero hacer un cambio simple: Cnecesito almacenar la cadena en algún lugar.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

Hola, copie el constructor y la posible asignación de memoria (ignore la optimización de cadena corta (SSO) ). Se supone que la semántica de movimiento de C++11 permite eliminar la construcción de copias innecesaria, ¿verdad? Y Apasa un temporal; no hay ninguna razón por la que Cdeba copiar los datos. Debería simplemente fugarse con lo que se le dio.

Excepto que no puede. Porque se necesita un const&.

Si cambio Cpara tomar su parámetro por valor, eso solo causa Bque se haga la copia en ese parámetro; No gano nada.

Entonces, si simplemente hubiera pasado strpor valor a través de todas las funciones, confiando en std::movemezclar los datos, no tendríamos este problema. Si alguien quiere conservarlo, puede hacerlo. Si no lo hacen, bueno.

¿Es más caro? Sí; pasar a un valor es más caro que utilizar referencias. ¿Es más barato que la copia? No para cadenas pequeñas con SSO. ¿Vale la pena hacerlo?

Depende de su caso de uso. ¿Cuánto odias las asignaciones de memoria?

Nicol Bolas avatar Apr 19 '2012 16:04 Nicol Bolas

¿Se acabaron los días en los que se pasaba const std::string & como parámetro?

No . Mucha gente lleva este consejo (incluido Dave Abrahams) más allá del dominio al que se aplica y lo simplifica para aplicarlo a todos std::string los parámetros. Pasar siemprestd::string por valor no es una "mejor práctica" para todos y cada uno de los parámetros y aplicaciones arbitrarios porque las optimizaciones que estos Las charlas/artículos se centran en aplicarse sólo a un conjunto restringido de casos .

Si devuelve un valor, muta el parámetro o toma el valor, pasar por valor podría ahorrarle costosas copias y ofrecer comodidad sintáctica.

Como siempre, pasar por la referencia constante ahorra muchas copias cuando no la necesita .

Ahora al ejemplo específico:

Sin embargo, inval sigue siendo mucho mayor que el tamaño de una referencia (que normalmente se implementa como un puntero). Esto se debe a que std::string tiene varios componentes, incluido un puntero al montón y un miembro char[] para optimización de cadenas cortas. Entonces me parece que pasar por referencia sigue siendo una buena idea. ¿Alguien puede explicar por qué Herb podría haber dicho esto?

Si el tamaño de la pila es una preocupación (y suponiendo que no esté integrado/optimizado), return_val+ inval> return_val-- IOW, el uso máximo de la pila se puede reducir pasando por valor aquí (nota: simplificación excesiva de las ABI). Mientras tanto, pasar por una referencia constante puede desactivar las optimizaciones. La razón principal aquí no es evitar el crecimiento de la pila, sino garantizar que la optimización se pueda realizar donde sea aplicable .

Los días de pasar por referencias constantes no han terminado: las reglas simplemente son más complicadas de lo que eran antes. Si el rendimiento es importante, será prudente considerar cómo pasar estos tipos, en función de los detalles que utilice en sus implementaciones.

justin avatar Apr 19 '2012 21:04 justin

Respuesta corta: ¡NO! Respuesta larga:

  • Si no va a modificar la cadena (tratarla como de solo lectura), pásela como const ref&.
    ( const ref&obviamente debe permanecer dentro del alcance mientras se ejecuta la función que lo utiliza)
  • Si planea modificarlo o sabe que quedará fuera del alcance (subprocesos) , páselo como value, no copie el const ref&interior del cuerpo de su función.

Había una publicación en cpp-next.com llamada "¡Si quieres velocidad, pasa por valor!" . El TL;DR:

Directriz : No copie los argumentos de su función. En su lugar, páselos por valor y deje que el compilador haga la copia.

TRADUCCIÓN de ^

No copie los argumentos de su función --- significa: si planea modificar el valor del argumento copiándolo en una variable interna, simplemente use un argumento de valor en su lugar .

Entonces, no hagas esto :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

hacer esto :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

Cuando necesita modificar el valor del argumento en el cuerpo de su función.

Sólo necesita saber cómo planea usar el argumento en el cuerpo de la función. Sólo lectura o NO... y si se mantiene dentro del alcance.

CodeAngry avatar Aug 23 '2013 16:08 CodeAngry

Esto depende en gran medida de la implementación del compilador.

Sin embargo, también depende de lo que uses.

Consideremos las siguientes funciones:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

Estas funciones se implementan en una unidad de compilación separada para evitar la inserción. Entonces:
1. Si pasa un literal a estas dos funciones, no verá mucha diferencia en el rendimiento. En ambos casos, se debe crear un objeto de cadena
2. Si pasa otro objeto std::string, foo2tendrá un rendimiento superior foo1, porque foo1hará una copia profunda.

En mi PC, usando g++ 4.6.1, obtuve estos resultados:

  • variable por referencia: 1000000000 iteraciones -> tiempo transcurrido: 2,25912 seg
  • variable por valor: 1000000000 iteraciones -> tiempo transcurrido: 27,2259 seg
  • literal por referencia: 100000000 iteraciones -> tiempo transcurrido: 9.10319 seg
  • literal por valor: 100000000 iteraciones -> tiempo transcurrido: 8,62659 seg
BЈовић avatar Apr 19 '2012 15:04 BЈовић

A menos que realmente necesite una copia, es razonable llevarla const &. Por ejemplo:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

Si cambia esto para tomar la cadena por valor, terminará moviendo o copiando el parámetro, y no es necesario. Copiar/mover no sólo es probablemente más costoso, sino que también introduce una nueva falla potencial; la copia/mover podría generar una excepción (por ejemplo, la asignación durante la copia podría fallar), mientras que tomar una referencia a un valor existente no puede hacerlo.

Si necesita una copia, pasar y devolver por valor suele ser (¿siempre?) la mejor opción. De hecho, generalmente no me preocuparía en C++03 a menos que descubra que las copias adicionales realmente causan un problema de rendimiento. La elisión de copia parece bastante confiable en los compiladores modernos. Creo que el escepticismo y la insistencia de la gente en que hay que comprobar la tabla de soporte del compilador para RVO está prácticamente obsoleto hoy en día.


En resumen, C++ 11 realmente no cambia nada a este respecto, excepto para las personas que no confiaban en la elisión de copia.

bames53 avatar Apr 19 '2012 15:04 bames53