Oí una reciente charla por Herb Sutter quien sugirió que las razones para pasar std::vector
y std::string
por 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_val
será 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, inval
todaví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::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?
Respuestas:
La razón por la que Herb dijo lo que dijo es por casos como este.
Digamos que tengo una función
A
que llama a la funciónB
, que llama a la funciónC
. YA
pasa una cuerda a travésB
y dentroC
.A
no sabe ni le importaC
; todo lo queA
sabe esB
. Es decir,C
es un detalle de implementación deB
.Digamos que A se define de la siguiente manera:
Si B y C toman la cadena
const&
, entonces se ve así:Todo bien y bien. Solo estás dando indicaciones, sin copiar, sin mover, todos están felices.
C
toma unconst&
porque no almacena la cadena. Simplemente lo usa.Ahora, quiero hacer un cambio simple:
C
necesita almacenar la cadena en algún lugar.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
A
pasa un temporal; No hay razónC
para 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
C
para tomar su parámetro por valor, eso solo haceB
que haga la copia en ese parámetro; No gano nada.Entonces, si hubiera pasado
str
por valor a través de todas las funciones, confiando enstd::move
barajar 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?
fuente
moved
de B a C en el caso por valor? Si B esB(std::string b)
y C es,C(std::string c)
entonces tenemos que llamarC(std::move(b))
a B o tenemosb
que permanecer sin cambios (por lo tanto, "sin moverse de") hasta salirB
. (Tal vez un compilador de optimización moverá la cadena bajo la regla de sib
no se usa después de la llamada, pero no creo que haya una garantía sólida). Lo mismo es cierto para la copia destr
am_str
. Incluso si un parámetro de función se inicializó con un valor r, es un valor l dentro de la función ystd::move
se requiere que se mueva desde ese valor.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 pasarstd::string
por 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:
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.
fuente
Esto depende en gran medida de la implementación del compilador.
Sin embargo, también depende de lo que use.
Consideremos las siguientes funciones:
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,
foo2
tendrá un rendimiento superiorfoo1
, porquefoo1
hará una copia profunda.En mi PC, usando g ++ 4.6.1, obtuve estos resultados:
fuente
Respuesta corta: ¡NO! Respuesta larga:
const ref&
.(
const ref&
obviamente, debe mantenerse dentro del alcance mientras se ejecuta la función que lo utiliza)value
, no copie elconst 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:
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 :
haz esto :
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.
fuente
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.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 largoref&
. # 4 Utilizopointers *
si un argumento es opcional para podernullptr
hacerlo.A menos que realmente necesite una copia, todavía es razonable tomarla
const &
. Por ejemplo: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.
fuente
noexcept
, pero los constructores de copia obviamente no lo son.Casi.
En C ++ 17, tenemos
basic_string_view<?>
, lo que nos lleva básicamente a un caso de uso limitado para losstd::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 unstd::string
valor por es más óptimo, ya que puedemove
salir del parámetro.Si alguien llamó a su función con una C sin procesar,
"string"
esto significa que solostd::string
se asigna un búfer, en lugar de dos en elstd::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 unastd::string
funcionalidad similar de manera más eficiente sin arriesgar ninguna asignación. Una cadena C sin procesar incluso puede convertirse en unastd::string_view
sin 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 questd::string
proporciona. En la práctica, este es un conjunto raro de requisitos.fuente
std::string
dominar 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?)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.std::string
no 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
inval
se 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, unaconst
referencia podría ser la mejor manera de hacerlo.fuente
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.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:
Para mí esto produce:
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:
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.
fuente
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 sonconst char[N]
tipos y requieren promociónstd::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 ().)
SALIDA:
fuente
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.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.La OMI que usa la referencia de C ++
std::string
es 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:
const std::string &
.std::string
comportamiento del constructor de copia.fuente
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.
Los puntos de referencia muestran que pasar
std::string
s 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.
fuente
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
f
que 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
f
argumentos adaptados a lvalue y rvalue respectivamente. Para ver por qué este es el caso, supongamos quef
toma un parámetro de valor, dondeT
hay algún tipo constructivo de copiar y mover:Llamar
f
con un argumento lvalue dará como resultado que se llame a un constructor de copia para construirx
, y que se llame a un constructor de movimiento para construiry
. Por otro lado, llamarf
con un argumento rvalue hará que se llame a un constructor de movimiento para construirx
, y que se llame a otro constructor de movimiento para construiry
.En general, la implementación óptima de los
f
argumentos de for lvalue es la siguiente:En este caso, solo se llama a un constructor de copia para construir
y
. La implementación óptima de losf
argumentos de rvalue es, de nuevo en general, como sigue: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
f
que 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ónf
que retiene una copia de su parámetro tendrá la forma: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:
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
y
y, 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
f
que retiene una copia tiene la forma:Entonces, solo una asignación de movimiento en este caso. Pasar un valor r a la versión de
f
eso toma una referencia constante solo cuesta una asignación en lugar de una asignación de movimiento. Hablando relativamente, la versión def
tomar 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
f
en caso de que opte por sobrecargar en la categoría de valor del argumento. El reenvío perfecto tiene el inconveniente de que sef
convierte 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).fuente
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.
fuente