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


604

Oí una reciente charla por Herb Sutter quien sugirió que las razones para pasar std::vectory std::stringpor const &están en gran medida han ido. 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, puede devolverse usando la semántica de movimiento, que es muy barata. Sin embargo, invaltodavía es mucho más grande que el tamaño de una referencia (que generalmente se implementa como un puntero). Esto se debe a que std::stringtiene 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?


89
Creo que la mejor respuesta a la pregunta es probablemente leer el artículo de Dave Abrahams al respecto en C ++ Siguiente . Agregaría que no veo nada sobre esto que califique como fuera de tema o no constructivo. Es una pregunta clara, sobre programación, para la cual hay respuestas objetivas.
Jerry Coffin

Fascinante, por lo que si va a tener que hacer una copia de todos modos, el paso por valor es probablemente más rápido que el paso por referencia.
Benj

3
@Sz. Soy sensible a las preguntas que se clasifican falsamente como duplicadas y cerradas. No recuerdo los detalles de este caso y no los he vuelto a revisar. En cambio, simplemente voy a eliminar mi comentario suponiendo que cometí un error. Gracias por traer esto a mi atención.
Howard Hinnant

2
@HowardHinnant, muchas gracias, siempre es un momento precioso cuando uno se encuentra con este nivel de atención y sensibilidad, ¡es muy refrescante! (Eliminaré el mío entonces, por supuesto)
Sz.

Respuestas:


393

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 la función B, que llama a la función C. Y Apasa una cuerda a través By dentro C. Ano sabe ni le importa C; todo lo que Asabe 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 const&, entonces se ve así:

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 dando indicaciones, sin copiar, sin mover, todos están felices. Ctoma un const&porque no almacena la cadena. Simplemente lo usa.

Ahora, quiero hacer un cambio simple: Cnecesita 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 asignación de memoria potencial (ignore la optimización de cadena corta (SSO) ). Se supone que la semántica de movimiento de C ++ 11 permite eliminar la construcción innecesaria de copias, ¿verdad? Y Apasa un temporal; No hay razón Cpara tener que copiar los datos. Simplemente debería 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 hace Bque haga la copia en ese parámetro; No gano nada.

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

¿Es más caro? Si; pasar 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 su caso de uso. ¿Cuánto odias las asignaciones de memoria?


2
Cuando dice que pasar a un valor es más costoso que usar referencias, eso es aún más costoso en una cantidad constante (independiente de la longitud de la cadena que se mueve) ¿verdad?
Neil G

3
@NeilG: ¿Entiende lo que significa "dependiente de la implementación"? Lo que estás diciendo está mal, porque depende de si SSO y cómo se implementa.
ildjarn

17
@ildjarn: en el análisis de pedidos, si el peor de los casos está vinculado por una constante, entonces todavía es tiempo constante. ¿No hay una cuerda pequeña más larga? ¿Esa cadena no toma una cantidad constante de tiempo para copiar? ¿No toman menos tiempo copiar todas las cadenas más pequeñas? Luego, la copia de cadenas para cadenas pequeñas es "tiempo constante" en el análisis de pedidos, a pesar de que las cadenas pequeñas requieren una cantidad de tiempo variable para copiar. El análisis del orden se refiere al comportamiento asintótico .
Neil G

8
@NeilG: Claro, pero su pregunta original era " eso es aún más caro en una cantidad constante (independientemente de la longitud de la cadena que se mueve) ¿verdad? " El punto que estoy tratando de hacer es que podría ser más costoso por diferentes cantidades constantes dependiendo de la longitud de la cadena, que se resume como "no".
ildjarn

13
¿Por qué la cadena sería movedde B a C en el caso por valor? Si B es B(std::string b)y C es, C(std::string c)entonces tenemos que llamar C(std::move(b))a B o tenemos bque permanecer sin cambios (por lo tanto, "sin moverse de") hasta salir B. (Tal vez un compilador de optimización moverá la cadena bajo la regla de si bno se usa después de la llamada, pero no creo que haya una garantía sólida). Lo mismo es cierto para la copia de stra m_str. Incluso si un parámetro de función se inicializó con un valor r, es un valor l dentro de la función y std::movese requiere que se mueva desde ese valor.
Pixelchemist

163

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

No se . Muchas personas toman este consejo (incluido Dave Abrahams) más allá del dominio al que se aplica, y lo simplifican para aplicarlo a todos los std::string parámetros: siempre pasar std::stringpor valor no es una "mejor práctica" para todos y cada uno de los parámetros y aplicaciones arbitrarias porque las optimizaciones las charlas / artículos se centran en aplicar 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 conveniencia sintáctica.

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

Ahora al ejemplo específico:

Sin embargo, inval todavía es 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 que incluyen 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 un problema (y suponiendo que no esté en línea / optimizado), return_val+ inval> return_val- IOW, el uso de la pila de pico puede reducirse pasando por valor aquí (nota: simplificación excesiva de las ABI). Mientras tanto, pasar por referencia constante puede deshabilitar 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 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.


3
En el uso de la pila, los ABI típicos pasarán una sola referencia en un registro sin uso de la pila.
ahcox

63

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

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

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. Luego:
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, 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 segundos
  • variable por valor: 1000000000 iteraciones -> tiempo transcurrido: 27.2259 segundos
  • literal por referencia: 100000000 iteraciones -> tiempo transcurrido: 9.10319 segundos
  • literal por valor: 100000000 iteraciones -> tiempo transcurrido: 8.62659 segundos

44
Lo que es más relevante es lo que está sucediendo dentro de la función: si se llama con una referencia, ¿tendría que hacer una copia interna que se pueda omitir al pasar por valor ?
Leftaroundabout

1
@leftaroundabout Sí, por supuesto. Mi suposición de que ambas funciones están haciendo exactamente lo mismo.
Bћовић

55
Ese no es mi punto. Si pasar por valor o por referencia es mejor depende de lo que esté haciendo dentro de la función. En su ejemplo, en realidad no está utilizando gran parte del objeto de cadena, por lo que la referencia es obviamente mejor. Pero si la tarea de la función fuera colocar la cadena en alguna estructura o realizar, por ejemplo, algún algoritmo recursivo que involucra múltiples divisiones de la cadena, pasar por valor podría en realidad guardar algunas copias, en comparación con pasar por referencia. Nicol Bolas lo explica bastante bien.
Leftaroundabout

3
Para mí, "depende de lo que hagas dentro de la función" es un mal diseño, ya que estás basando la firma de la función en lo interno de la implementación.
Hans Olsson

1
Puede ser un error tipográfico, pero los dos últimos tiempos literales tienen 10 veces menos bucles.
TankorSmash

54

Respuesta corta: ¡NO! Respuesta larga:

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

Hubo una publicación en cpp-next.com llamada "Quiero velocidad, pasa por valor!" . 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 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;
}

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

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


2
Recomienda pasar por referencia en algunos casos, pero señala una directriz que recomienda siempre pasar por valor.
Keith Thompson el

3
@KeithThompson No copie los argumentos de su función. Los medios no copian const ref&a una variable interna para modificarlo. Si necesita modificarlo ... haga que el parámetro sea un valor. Está bastante claro para mi persona que no habla inglés.
CodeAngry el

55
@KeithThompson La cita de la guía (no copie los argumentos de su función. En su lugar, páselos por valor y deje que el compilador haga la copia) se copia de esa página. Si eso no es lo suficientemente claro, no puedo ayudar. No confío totalmente en los compiladores para tomar las mejores decisiones. Prefiero ser muy claro acerca de mis intenciones en la forma en que defino los argumentos de la función. # 1 Si es de solo lectura, es a const ref&. # 2 Si necesito escribirlo o sé que se sale del alcance ... Uso un valor. # 3 Si necesito modificar el valor original, paso de largo ref&. # 4 Utilizo pointers *si un argumento es opcional para poder nullptrhacerlo.
CodeAngry el

11
No estoy tomando partido por la cuestión de si pasar por valor o por referencia. Mi punto es que usted aboga por pasar por referencia en algunos casos, pero luego cita (aparentemente para apoyar su posición) una guía que recomienda siempre pasar por valor. Si no está de acuerdo con la directriz, es posible que desee decirlo y explicar por qué. (Los enlaces a cpp-next.com no funcionan para mí.)
Keith Thompson

44
@KeithThompson: Estás parafraseando mal la directriz. No es "siempre" pasar por valor. Para resumir, era "Si hubiera hecho una copia local, use pasar por valor para que el compilador realice esa copia por usted". No está diciendo usar el paso por valor cuando no ibas a hacer una copia.
Ben Voigt

43

A menos que realmente necesite una copia, todavía 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 es probable que copiar / mover sea más costoso, sino que también introduce una nueva falla potencial; la copia / movimiento podría arrojar 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 usted no necesita una copia a continuación, pasar o retornar por el valor es por lo general (siempre?) La mejor opción. De hecho, generalmente 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 los compiladores modernos. Creo que el escepticismo y la insistencia de la gente en que debe verificar su tabla de compatibilidad de compiladores para RVO es en la actualidad obsoleta.


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


2
Los constructores de movimiento generalmente se implementan con noexcept, pero los constructores de copia obviamente no lo son.
Leftaroundabout

25

Casi.

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

La existencia de semántica de movimiento ha eliminado un caso de uso std::string const&: si está planeando almacenar el parámetro, tomar un std::stringvalor por es más óptimo, ya que puede movesalir del parámetro.

Si alguien llamó a su función con una C sin procesar, "string"esto significa que solo std::stringse asigna un búfer, en lugar de dos en el std::string const&caso.

Sin embargo, si no tiene la intención de hacer una copia, seguir utilizando std::string const&es útil en C ++ 14.

Con std::string_view, siempre que no pase dicha cadena a una API que espere '\0'buffers de caracteres terminados en estilo C , puede obtener una std::stringfuncionalidad similar de manera más eficiente sin arriesgar ninguna asignación. Una cadena C sin procesar incluso puede convertirse en una std::string_viewsin ninguna asignación o copia de caracteres.

En ese punto, el uso de 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 con terminación nula, y necesita las funciones de cadena de nivel superior que std::stringproporciona. En la práctica, este es un conjunto raro de requisitos.


2
Aprecio esta respuesta, pero quiero señalar que sufre (como muchas respuestas de calidad) de un sesgo específico del dominio. A saber: "En la práctica, este es un conjunto raro de requisitos" ... en mi propia experiencia de desarrollo, estas limitaciones, que parecen anormalmente estrechas para el autor, se cumplen, literalmente, todo el tiempo. Vale la pena señalar esto.
fish2000

1
@ fish2000 Para ser claros, para std::stringdominar no solo necesita algunos de esos requisitos, sino todos . Cualquiera o incluso dos de esos es, lo admito, común. ¿Quizás necesita comúnmente los 3 (por ejemplo, está analizando un argumento de cadena para elegir a qué API de C va a pasarlo al por mayor?)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont Es una cuestión de YMMV, pero es un caso de uso frecuente si (por ejemplo) está programando contra POSIX u otras API donde la semántica de C-string es, como, el mínimo común denominador. Debo decir realmente que me encanta std::string_view, como usted señala, "una cadena C sin procesar incluso se puede convertir en std :: string_view sin ninguna asignación o copia de caracteres", algo que vale la pena recordar para aquellos que usan C ++ en el contexto de tal uso de API, de hecho.
fish2000

1
@ fish2000 "'Una cadena C sin procesar incluso puede convertirse en std :: string_view sin ninguna asignación o copia de caracteres', algo que vale la pena recordar". De hecho, pero omite la mejor parte: en el caso de que la cadena sin formato sea un literal de cadena, ¡ ni siquiera requiere un tiempo de ejecución strlen () !
Don Hatch

17

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

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


2
Punto interesante acerca de que el constructor de copias es lo suficientemente inteligente como para no preocuparse por el SSO si no lo está usando. Probablemente correcto, voy a tener que verificar si eso es cierto ;-)
Benj

3
@Benj: Viejo comentario, lo sé, pero si SSO es lo suficientemente pequeño, copiarlo incondicionalmente es más rápido que hacer una rama condicional. Por ejemplo, 64 bytes es una línea de caché y se puede copiar en una cantidad de tiempo realmente trivial. Probablemente 8 ciclos o menos en x86_64.
Zan Lynx

Incluso si el constructor de copia no copia el SSO, std::string<>se asignan 32 bytes desde la pila, 16 de los cuales deben inicializarse. Compare esto con solo 8 bytes asignados e inicializados para una referencia: es el doble de la cantidad de trabajo de la CPU, y ocupa cuatro veces más espacio de caché que no estará disponible para otros datos.
cmaster - reinstalar a monica el

Ah, y olvidé hablar sobre pasar argumentos de función en registros; eso reduciría el uso de pila de la referencia a cero para la última llamada ...
cmaster - restablecer monica

16

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

Aquí hay un código para medir lo que se pide:

#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 produce:

$ 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 requiere solo una sobrecarga, pero cuesta una construcción de movimiento adicional al pasar valores y valores x. Esto puede o no ser aceptable para cualquier situación dada. Ambas soluciones tienen ventajas y desventajas.


1
std :: string es una clase de biblioteca estándar. Ya es a la vez móvil y copiable. No veo cómo esto es relevante. El OP pregunta más sobre el rendimiento del movimiento frente a las referencias , no el rendimiento del movimiento frente a la copia.
Nicol Bolas

3
Esta respuesta cuenta el número de movimientos y copias que sufrirá un std :: string bajo el diseño de paso por valor descrito por Herb y Dave, en vez de pasar por referencia con un par de funciones sobrecargadas. Utilizo el código del OP en la demostración, excepto para sustituir en una cadena ficticia para gritar cuando se copia / mueve.
Howard Hinnant

Probablemente deberías optimizar el código antes de realizar las pruebas ...
The Paramagnetic Croissant

3
@TheParamagneticCroissant: ¿Obtuviste resultados diferentes? Si es así, ¿usando qué compilador con qué argumentos de línea de comando?
Howard Hinnant

14

Herb Sutter todavía está registrado, junto con Bjarne Stroustroup, en recomendar const std::string&como 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 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 debe al hecho de que los literales de cadena son const char[N]tipos y requieren promoción std::string.

El siguiente código ilustra la trampa y la solución alternativa, 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 mantiene 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

Este es un problema diferente y WidgetBadRef no necesita tener un const y parámetro para salir mal. La pregunta es si WidgetSafeCopy solo tomó un parámetro de cadena ¿sería más lento? (Creo que la copia temporal para el miembro es ciertamente más fácil de detectar)
Superfly Jon

Tenga en cuenta que T&&no siempre es una referencia universal; de hecho, std::string&&siempre será una referencia de valor, y nunca una referencia universal, porque no se realiza ninguna deducción de tipo . Por lo tanto, el consejo de Stroustroup & Sutter no entra en conflicto con el de Meyers.
Justin Time - Restablece a Mónica el

@JustinTime: gracias; Eliminé la oración final incorrecta alegando, en efecto, que std :: string && sería una referencia universal.
circlepi314

@ circlepi314 De nada. Es una mezcla fácil de hacer, a veces puede ser confuso si alguna T&&es una referencia universal deducida o una referencia de valor no deducida; probablemente habría sido más claro si introdujeran un símbolo diferente para referencias universales (como &&&, como una combinación de &y &&), pero eso probablemente se vería tonto.
Justin Time - Restablece a Mónica el

8

La OMI que usa la referencia de C ++ std::stringes una optimización local rápida y corta, mientras que el uso del paso 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 desde el exterior a las funciones internas, sabe lo que hace el código, puede usar la referencia const std::string &.
  2. Si escribe el código de la biblioteca o usa mucho código de la biblioteca donde se pasan las cadenas, es probable que gane más en sentido global al confiar en el std::stringcomportamiento del constructor de copia.

6

Consulte "Herb Sutter" ¡Regreso a lo básico! Fundamentos del estilo moderno de C ++ ” . Entre otros temas, repasa los consejos para pasar parámetros que se han dado en el pasado y las nuevas ideas que vienen con C ++ 11 y analiza específicamente idea de pasar cadenas por valor.

diapositiva 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á obligando a hacer siempre una copia completa (y luego moverlo a su lugar), mientras que la const&versión actualizará la cadena anterior que puede reutilizar el búfer ya asignado.

Vea su diapositiva 27: Para las funciones de "set", la opción 1 es la misma de siempre. La opción 2 agrega una sobrecarga para la referencia de valor r, pero esto genera 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 debe cambiar 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 quieres ver cuán profundo puedes llegar preocupándote por esto, mira la presentación de Nicolai Josuttis y buena suerte con eso ( "Perfecto - ¡Hecho!" N veces después de encontrar fallas en la versión anterior. ¿Alguna vez has estado allí?)


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


3

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

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

Este enfoque general solo agrega la sobrecarga de un constructor de movimiento para los argumentos lvalue y rvalue en comparación con una implementación óptima de fargumentos adaptados a lvalue y rvalue respectivamente. Para ver por qué este es el caso, supongamos que ftoma un parámetro de valor, donde Thay algún tipo constructivo de copiar y mover:

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

Llamar fcon un argumento lvalue dará como resultado que se llame a un constructor de copia para construir x, y que se llame a un constructor de movimiento para construir y. Por otro lado, llamar fcon 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 los fargumentos de for lvalue es la siguiente:

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

En este caso, solo se llama a un constructor de copia para construir y. La implementación óptima de los fargumentos de rvalue es, de nuevo en general, como sigue:

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

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

Por lo tanto, un compromiso razonable es tomar un parámetro de valor y hacer que un constructor de movimiento adicional llame a los argumentos 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 gastos generales que un enfoque de paso por referencia general. 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 a una asignación solamente, que es potencialmente mucho más barata que el constructor de copia más la asignación de movimiento requerida para el enfoque de paso por valor. La razón de esto es que la asignación podría 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 fque retiene una copia tiene la forma:

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

Entonces, solo una asignación de movimiento en este caso. Pasar un valor r a la versión de feso 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.

Por lo tanto, en general, para la implementación más óptima, deberá sobrecargar o realizar 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 sea virtual, y da como resultado un código significativamente más complejo si desea obtener el 100% correcto (consulte la charla para obtener los detalles sangrientos).


Vea los hallazgos de Herb Sutter en mi nueva respuesta: solo haga eso cuando mueva la construcción , no mueva la asignación.
JDługosz

1
@ JDługosz, gracias por el puntero a la charla de Herb, lo acabo de ver y revisé por completo mi respuesta. No estaba al tanto del (movimiento) asignar consejo.
Ton van den Heuvel

esa cifra y consejos están en el documento de las Pautas estándar , ahora.
JDługosz

1

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 referencias". Simplemente no hay forma, en C ++, de decir qué miembros son "constantes". O todos lo son, o ninguno lo es.

Con el fin de hackear este problema de lenguaje, STL podría permitir que "C ()" en su ejemplo haga una copia semántica de movimiento de todos modos , e ignore obedientemente la "constante" con respecto al recuento de referencias (mutable). Mientras esté bien especificado, esto estaría bien.

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

Dado que C ++ no ofrece "granularidad de const de clase derivada" aquí, escribir una buena especificación y hacer un nuevo y brillante objeto de "cadena móvil constante" (cmstring) es la mejor solución que he visto.


@BenVoigt sip ... en lugar de descartar, debería ser mutable ... pero no puede cambiar un miembro STL a mutable en una clase derivada.
Erik Aronesty
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.