dev - c++17




¿Los días de pasar const std:: string & como un parámetro terminado? (9)

¿Los días de pasar const std :: string & como un parámetro terminado?

No Mucha gente toma este consejo (incluido Dave Abrahams) más allá del dominio al que se aplica, y lo simplifica para que se aplique a todos std::string parámetros de std::string std::string pasar siempre std::string por valor no es una "práctica recomendada" para todos. Parámetros y aplicaciones arbitrarios porque las optimizaciones en las que se centran estas conversaciones / artículos se aplican solo a un conjunto restringido de casos .

Si está devolviendo un valor, mutando el parámetro o tomando el valor, entonces pasar por valor podría ahorrar una copia costosa y ofrecer comodidad sintáctica.

Como siempre, pasar por referencia constante guarda muchas copias cuando no necesita una copia .

Ahora al ejemplo específico:

Sin embargo, inval es aún mucho más grande que el tamaño de una referencia (que generalmente 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 la optimización de cadenas cortas. Así que me parece que pasar por referencia sigue siendo una buena idea. ¿Alguien puede explicar por qué Herb pudo haber dicho esto?

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

Los días de pasar por referencia constante no han terminado, las reglas son más complicadas de lo que alguna vez fueron. Si el rendimiento es importante, será prudente considerar cómo pasa estos tipos, según los detalles que use en sus implementaciones.

Escuché una reciente charla de Herb Sutter, quien sugirió que las razones para pasar std::vector y std::string by const & gran medida han desaparecido. Sugirió que escribir una función como la siguiente ahora es preferible:

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

Entiendo que return_val será un valor en el punto en el que la función regresa y, por lo tanto, se puede devolver utilizando la semántica de movimientos, que son muy baratos. Sin embargo, inval es aún mucho más grande que el tamaño de una referencia (que generalmente 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 la optimización de cadenas cortas. Así que me parece que pasar por referencia sigue siendo una buena idea.

¿Alguien puede explicar por qué Herb pudo haber dicho esto?


A menos que realmente necesite una copia, todavía es razonable tomar 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 hay necesidad de eso. No solo es más caro copiar / mover, sino que también introduce un nuevo fallo potencial; la copia / movimiento podría lanzar 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.

Si necesita una copia, entonces pasar y devolver por valor suele ser (¿siempre?) La mejor opción. De hecho, en general no me preocuparía en C ++ 03 a menos que encuentre que las copias adicionales realmente causan un problema de rendimiento. Copiar elision parece bastante confiable en compiladores modernos. Creo que el escepticismo y la insistencia de la gente en que tiene que revisar su tabla de compatibilidad del compilador para RVO es en su mayoría obsoleta en la actualidad.

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


Como @ JDługosz señala en los comentarios, Herb da otros consejos en otra charla (¿más tarde?), Vea más o menos desde aquí: https://youtu.be/xnqTKD8uD64?t=54m50s .

Su consejo se reduce a usar solo los parámetros de valor para una función f que toma los llamados argumentos de hundimiento, asumiendo que moverá la construcción desde estos argumentos de hundimiento.

Este enfoque general solo agrega la sobrecarga de un constructor de movimiento para los argumentos tanto lvalue como rvalue en comparación con una implementación óptima de f adaptada a los argumentos lvalue y rvalue respectivamente. Para ver por qué este es el caso, suponga que f toma un parámetro de valor, donde T es una copia y mueve el tipo constructible:

void f(T x) {
  T y{std::move(x)};
}

Si se llama a f con un argumento lvalue, se llamará a un constructor de copia para construir x , y se llamará a un constructor de movimiento para construir y . Por otro lado, llamar a f con un argumento rvalue hará que se llame a un constructor de movimiento para construir x , y que se llame a otro constructor de movimiento para construir y .

En general, la implementación óptima de f para lvalue argumentos es la siguiente:

void f(const T& x) {
  T y{x};
}

En este caso, solo se llama un constructor de copia para construir y . La implementación óptima de f para rvalue argumentos es, nuevamente en general, como sigue:

void f(T&& x) {
  T y{std::move(x)};
}

En este caso, solo se llama un constructor de movimiento para construir y .

Por lo tanto, un compromiso razonable es tomar un parámetro de valor y tener un constructor de movimiento adicional para los argumentos de valor lvalue o rvalue con respecto a la implementación óptima, que también es el consejo dado en la charla de Herb.

Como @ JDługosz señaló en los comentarios, pasar por valor solo tiene sentido para las funciones que construirán algún objeto a partir del argumento sumidero.Cuando tiene una función fque copia su argumento, el enfoque de paso por valor tendrá más sobrecarga que un enfoque general de paso por referencia. El enfoque de paso por valor para una función fque retiene una copia de su parámetro tendrá la forma:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

En este caso, hay una construcción de copia y una asignación de movimiento para un argumento lvalue, y una construcción de movimiento y una asignación de movimiento para un argumento rvalue. El caso más óptimo para un argumento lvalue es:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

Esto se reduce solo a una asignación, que es potencialmente mucho más económica que el constructor de copia más la asignación de movimiento requerida para el enfoque de paso por valor. El motivo de esto es que la asignación puede reutilizar la memoria asignada existente yy, por lo tanto, evitar (des) asignaciones, mientras que el constructor de copias generalmente asignará memoria.

Para un argumento rvalue, la implementación más óptima para fretener una copia tiene la forma:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Por lo tanto, sólo una asignación de movimiento en este caso. Pasar un valor a la versión de fque toma una referencia constante solo cuesta una asignación en lugar de una asignación de movimiento. Hablando relativamente, la versión de ftomar una referencia constante en este caso como la implementación general es preferible.

Entonces, en general, para la implementación más óptima, necesitarás sobrecargar o hacer algún tipo de reenvío perfecto como se muestra en la charla. El inconveniente es una explosión combinatoria en el número de sobrecargas requeridas, dependiendo del número de parámetros fen caso de que opte por sobrecargar en la categoría de valor del argumento. El reenvío perfecto tiene el inconveniente de que se fconvierte en una función de plantilla, lo que evita que se vuelva virtual, y da como resultado un código significativamente más complejo si desea hacerlo al 100% (consulte la información detallada sobre los detalles sangrientos).


Consulte "Herb Sutter" ¡De vuelta a lo básico! Lo esencial del estilo moderno de C ++ . Entre otros temas, revisa los consejos sobre el paso de parámetros que se han dado en el pasado y las nuevas ideas que vienen con C ++ 11 y analiza específicamente el Idea de pasar cadenas por valor.

Los puntos de referencia muestran que pasar std::string s por valor, en los casos en que la función lo copiará de todas formas, puede ser significativamente más lento.

Esto se debe a que está obligando a que siempre haga una copia completa (y luego se mueva a su lugar), mientras que const& version actualizarán la cadena antigua que puede reutilizar el búfer ya asignado.

Vea su diapositiva 27: Para las funciones de "ajuste", la opción 1 es la misma que siempre fue. La opción 2 agrega una sobrecarga para la referencia rvalue, pero esto da una explosión combinatoria si hay múltiples parámetros.

Es solo para los parámetros de "hundimiento" donde se debe crear una cadena (no se ha cambiado su valor existente) que el truco de paso por valor es válido. Es decir, constructores en los que el parámetro inicializa directamente el miembro del tipo coincidente.

Si desea ver qué tan profundo puede llegar a preocuparse por esto, vea la presentación de Nicolai Josuttis y buena suerte con eso ( “¡Perfecto, listo!” N veces después de encontrar fallas en la versión anterior. ¿Alguna vez ha estado allí?)

Esto también se resume como ⧺F.15 en las Pautas estándar.


He copiado / pegado la respuesta de esta pregunta aquí, y cambié los nombres y la ortografía para que se ajusten a esta pregunta.

Aquí está el código para medir lo que se pregunta:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Para mí esto da como resultado:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

La siguiente tabla resume mis resultados (usando clang -std = c ++ 11). El primer número es el número de construcciones de copia y el segundo número es el número de construcciones de movimiento:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

La solución de paso por valor solo requiere una sobrecarga, pero cuesta una construcción de movimiento adicional al pasar lvalues ​​y xvalues. Esto puede o no ser aceptable para cualquier situación dada. Ambas soluciones tienen ventajas y desventajas.


Herb Sutter aún está registrado, junto con Bjarne Stroustroup, al recomendar const std::string& como un tipo de parámetro; vea https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

Hay una trampa que no se menciona en ninguna de las otras respuestas aquí: si pasa un literal de cadena a const std::string& parameters, pasará una referencia a una cadena temporal, creada sobre la marcha para contener los caracteres de el literal Si luego guarda esa referencia, no será válida una vez que se desasigne la cadena temporal. Para estar seguro, debe guardar una copia , no la referencia. El problema se deriva del hecho de que los literales de cadena son tipos const char[N] , que requieren promoción a std::string .

El código a continuación ilustra la trampa y la solución, junto con una opción de menor eficiencia: sobrecargar con un método const char* , como se describe en ¿Existe una manera de pasar un literal de cadena como referencia en C ++ ?

(Nota: Sutter y Stroustroup informan que si conserva una copia de la cadena, también proporcione una función sobrecargada con un parámetro && y std :: move ()).

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

SALIDA:

const char * constructor
const std::string& constructor

Second string
Second string

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

Digamos que tengo la función A que llama a la función B , que llama a la función C Y A pasa una cadena a través de B y en C A no sabe ni se preocupa por C ; todo lo que A conoce es B Es decir, C es un detalle de implementación de B

Digamos que A se define como sigue:

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

Si B y C toman la cadena por const& , entonces se ve algo como esto:

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

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

Todo bien y bien. Solo estás pasando punteros, sin copiar, sin moverte, todos están felices. C toma una const& porque no almacena la cadena. Simplemente lo usa.

Ahora, quiero hacer un cambio simple: C necesita 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) ). La semántica de movimientos de C ++ 11 se supone que hace posible eliminar construcciones innecesarias de copias, ¿verdad? Y A pasa un temporal; no hay razón por la que C deba copiar los datos. Simplemente debe fugarse con lo que se le dio.

Excepto que no puede. Porque hace falta una const& .

Si cambio C para tomar su parámetro por valor, eso solo hace que B haga la copia en ese parámetro; No gano nada

Entonces, si acabara de pasar str por valor a través de todas las funciones, confiando en std::move para barajar los datos, no tendríamos este problema. Si alguien quiere aferrarse a ello, puede. Si no lo hacen, bueno.

¿Es más caro? Sí; mudarse a un valor es más caro que usar referencias. ¿Es menos costoso que la copia? No para cuerdas pequeñas con SSO. ¿Vale la pena hacerlo?

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


Respuesta corta: ¡NO! Respuesta larga:

  • Si no modifica la cadena (el tratamiento es de solo lectura), páselo como const ref& .
    (la const ref& obviamente necesita mantenerse dentro del alcance mientras se ejecuta la función que la utiliza)
  • Si planea modificarlo o sabe que saldrá del alcance (hilos) , páselo como un value , no copie la const ref& dentro de su cuerpo de función.

Hubo una publicación en cpp-next.com llamada "Want speed, pass by value!" . El TL; DR:

Pauta : 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 sus argumentos de función --- significa: si planea modificar el valor del argumento copiándolo en una variable interna, 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;
}

haz esto

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

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

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


El problema es que "const" es un calificador no granular. Lo que generalmente se entiende por "const string ref" es "no modifique esta cadena", no "no modifique el recuento de referencia". Simplemente no hay forma, en C ++, de decir qué miembros son "const". O bien todos son, o ninguno de ellos es.

Para solucionar este problema de lenguaje, STL podría permitir que "C ()" en su ejemplo haga una copia semántica de movimiento de todos modos , e ignorar obedientemente el "const" con respecto al recuento de referencia (mutable). Mientras estuviera bien especificado, esto estaría bien.

Como STL no lo hace, tengo una versión de una cadena que const_casts <> lejos del contador de referencia (no hay manera de hacer que algo mutable en una jerarquía de clases sea retroactiva), y - he aquí que - puedes pasar libremente las referencias de cmstring como constantes, y haga copias de ellos en funciones profundas, durante todo el día, sin fugas ni problemas.

Como C ++ no ofrece "granularidad constante de clase derivada", escribir una buena especificación y crear un nuevo objeto brillante de "cadena móvil" (cm) es la mejor solución que he visto.





c++11