Pregunta ¿Están pasando los días de pasar const std :: string & como parámetro?


Escuché una reciente charla de Herb Sutter que sugirió que las razones para aprobar std::vector y std::string por const & se han ido en gran parte. Sugirió que escribir una función como la siguiente es ahora preferible:

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

Entiendo que el return_val será un valor r en el punto en que la función retorna y, por lo tanto, puede devolverse utilizando la semántica de movimiento, que es muy económica. Sin embargo, inval sigue siendo mucho más grande que el tamaño de una referencia (que generalmente se implementa como un puntero). Esto es porque std::string tiene varios componentes, incluyendo un puntero en el 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?


513
2018-04-19 15:20


origen


Respuestas:


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

Digamos que tengo la función A que llama función B, que llama función C. Y A pasa una cuerda a través de B y en C. A no sabe o no le importa C; todas A sabe acerca de es B. Es decir, C es 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 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á pasando punteros, sin copiar, sin moverse, 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 alguna parte

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 el Optimización de cadenas cortas (SSO)) Se supone que la semántica del movimiento de C ++ 11 permite eliminar la construcción innecesaria de copias, ¿verdad? Y A pasa un temporal; no hay razón por la cual C debería tener que dupdo los datos. Debería desaparecer con lo que se le dio.

Excepto que no puede. Porque toma un const&.

Si cambio C tomar su parámetro por valor, que solo causa B para hacer 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 él, puede hacerlo. Si no lo hacen, bueno.

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

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


349
2018-04-19 16:41



¿Están pasando los días de pasar const std :: string & como parámetro?

No. Muchas personas toman este consejo (incluido Dave Abrahams) más allá del dominio al que se aplica, y lo simplifican para aplicar a todas  std::string parámetros - Siempre paso std::string por valor no es una "mejor práctica" para todos los 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 costosas copias y ofrecer comodidad sintáctica.

Como siempre, pasar por referencia constante ahorra mucha copia cuando no necesitas una copia.

Ahora al ejemplo específico:

Sin embargo, el valor de invalidez es todavía bastante 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 en el 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?

Si el tamaño de la pila es una preocupación (y suponiendo que esto no está en línea / optimizado), return_val + inval > return_val - IOW, el uso pico de pila puede ser reducido pasando aquí valor (nota: simplificación excesiva de ABI). Mientras tanto, pasar por referencia de referencia 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 es aplicable.

Los días de pasar por la 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 utiliza en sus implementaciones.


125
2018-04-19 21:07



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 creación de líneas. Entonces :
1. Si pasa un literal a estas dos funciones, no verá mucha diferencia en las actuaciones. En ambos casos, se debe crear un objeto de cadena
2. Si pasa otro objeto std :: string, foo2 superará foo1, porque foo1 Hará 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 sec

55
2018-04-19 15:44



A menos que realmente necesite una copia, es razonable tomarla 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 la copia / movimiento probablemente sea más costosa, sino que también introduce una nueva falla 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 tu hacer Necesita una copia y luego pasar y regresar por valor suele ser (¿siempre?) la mejor opción. De hecho, en general no me preocuparía en C ++ 03 a menos que descubras 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 deba verificar su tabla de compatibilidad de compiladores para RVO es obsoleta en la actualidad.


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


41
2018-04-19 15:29



Respuesta corta: ¡NO! Respuesta larga:

  • Si no va a modificar la cadena (el tratamiento es de solo lectura), páselo como const ref&.
    (el const ref& obviamente necesita mantenerse dentro del alcance mientras se ejecuta la función que lo utiliza)
  • Si planea modificarlo o sabe que saldrá del alcance (trapos), pásalo como un value, no copies el const ref& dentro de tu cuerpo de función.

Hubo una publicación en cpp-next.com llamado "¿Quieres velocidad, pasa por valor?". El TL; DR:

Guía: No copie los argumentos de su función. En cambio, 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, solo use un argumento de valor en su lugar.

Asi que, 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 necesite modificar el valor del argumento en su cuerpo de 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.


40
2017-08-23 16:33



std::string no es Plain Old Data (POD), y su tamaño en bruto no es lo más relevante que haya existido. Por ejemplo, si pasa una cadena que está por encima de la longitud de SSO y se asigna en el montón, esperaría que el constructor de copia no copie el almacenamiento de SSO.

La razón por la que esto se recomienda es porque inval se construye a partir de la expresión del argumento y, por lo tanto, siempre se mueve o se copia según corresponda; no hay pérdida de rendimiento, suponiendo que se necesita la propiedad del argumento. Si no lo haces, una const la referencia aún podría ser la mejor manera de hacerlo.


16
2018-04-19 15:24



Casi.

En C ++ 17, tenemos basic_string_view<?>, lo que nos lleva básicamente a un caso de uso estrecho para std::string const& parámetros.

La existencia de semántica de movimiento ha eliminado un caso de uso para std::string const& - si está planeando almacenar el parámetro, tomando una std::string por valor es más óptimo, como puedes move fuera del parámetro.

Si alguien llamó a tu función con una C en bruto "string" esto significa solo uno std::string el búfer se asigna alguna vez, a diferencia de dos en el std::string const& caso.

Sin embargo, si no tiene la intención de hacer una copia, tomando por std::string const& sigue siendo útil en C ++ 14.

Con std::string_view, siempre y cuando no esté pasando dicha cadena a una API que espera estilo C '\0'búferes de caracteres terminados, puede obtener más eficientemente std::string como la funcionalidad sin arriesgar ninguna asignación. Una cadena C sin procesar puede incluso convertirse en una std::string_view sin ninguna asignación o copia de caracteres.

En ese punto, el uso para std::string const& es cuando no está copiando los datos al por mayor y los va a pasar a una API de estilo C que espera un búfer terminado nulo, y necesita las funciones de cadena de nivel superior que std::stringproporciona. En la práctica, este es un conjunto raro de requisitos.


16
2018-01-26 03:09



Copié / pegué la respuesta de esta pregunta aquí, y cambió los nombres y la ortografía para adaptarse a esta pregunta.

Aquí hay un código para medir lo que se está preguntando:

#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í este producto:

$ 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 tabla a continuación resume mis resultados (usando clang -std = c ++ 11). El primer número es el número de construcciones de copias y el segundo número es el número de construcciones de movimientos:

+----+--------+--------+---------+
|    | 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.


15
2018-04-19 16:13



Herb Sutter todavía está registrado, junto con Bjarne Stroustroup, en la recomendación const std::string& como un tipo de parámetro; ver 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 una cadena literal a un const std::string& parámetro, pasará una referencia a una cadena temporal, creada sobre la marcha para contener los caracteres del literal. Si luego guarda esa referencia, será inválida una vez que la cadena temporal se desasigna. Para estar seguro, debes guardar un dupdono la referencia El problema radica en el hecho de que los literales de cadena son const char[N] tipos, que requieren promoción a std::string.

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

(Nota: Sutter y Stroustroup aconsejan 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

13
2018-01-27 15:50



IMO utilizando la referencia de C ++ para std::string es una optimización local rápida y corta, mientras que usar pasar por valor podría ser (o no) una mejor optimización global.

Entonces la respuesta es: depende de las circunstancias:

  1. Si escribe todo el código de las funciones de afuera hacia adentro, sabe lo que hace el código, puede usar la referencia const std::string &.
  2. Si escribe el código de la biblioteca o utiliza un gran código de biblioteca donde se pasan las cadenas, es probable que gane más en el sentido global confiando en std::string copiar el comportamiento del constructor

7
2018-04-25 09:26



Ver "Herb Sutter" Volver a lo básico! Esencial del estilo moderno de C ++ ". Entre otros temas, revisa el consejo de aprobación de parámetros que se ha dado en el pasado, y las nuevas ideas que vienen con C ++ 11 y específicamente analiza la idea de pasar cadenas por valor.

slide 24

Los puntos de referencia muestran que pasar std::strings por valor, en los casos en que la función lo copiará de todos modos, ¡puede ser significativamente más lento!

Esto se debe a que lo está forzando a hacer siempre una copia completa (y luego a colocarlo en su lugar), mientras que el const& la versión actualizará la cadena anterior 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. La opción 2 agrega una sobrecarga para la referencia de valor real, pero esto genera una explosión combinatoria si hay múltiples parámetros.

Es solo para los parámetros de "sumidero" donde se debe crear una cadena (no se modificó su valor existente) para que el truco de pasar por valor sea válido. Es decir, constructores en el que el parámetro inicializa directamente el miembro del tipo coincidente.

Si quieres ver qué tan profundo puedes llegar a preocuparte por esto, mira Nicolai Josuttis presentación y buena suerte con eso ("Perfecto, listo!" n veces después de encontrar un error con la versión anterior. ¿Has estado alguna vez allí?


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


3
2018-04-25 18:32