with what pointer passing pass parameter does and c++ c++11 parameter-passing pass-by-reference pass-by-value

what - Tipos de vista C++: ¿pasar por const y por valor?



reference function c++ (8)

Esto surgió recientemente en una discusión de revisión de código, pero sin una conclusión satisfactoria. Los tipos en cuestión son análogos al C ++ string_view TS. Son envoltorios simples que no poseen alrededor de un puntero y una longitud, decorados con algunas funciones personalizadas:

#include <cstddef> class foo_view { public: foo_view(const char* data, std::size_t len) : _data(data) , _len(len) { } // member functions related to viewing the ''foo'' pointed to by ''_data''. private: const char* _data; std::size_t _len; };

Se planteó la cuestión de si existe un argumento para preferir pasar tales tipos de vista (incluidos los próximos string_view y array_view) por valor o por referencia constante.

Los argumentos a favor de pasar por valor equivalen a "menos escritura", "puede mutar la copia local si la vista tiene mutaciones significativas" y "probablemente no sea menos eficiente".

Los argumentos a favor de la referencia paso a paso equivalían a ''más idiomático para pasar objetos por const y'', y ''probablemente no menos eficiente''.

¿Hay alguna consideración adicional que pueda cambiar el argumento de manera concluyente de una manera u otra en términos de si es mejor pasar tipos de vista idiomáticos por valor o por referencia constante?

Para esta pregunta es seguro asumir la semántica de C ++ 11 o C ++ 14 y las cadenas de herramientas y arquitecturas de objetivos suficientemente modernas, etc.


Además de lo que ya se ha dicho aquí a favor de pasar por valor, los optimizadores modernos de C ++ luchan con los argumentos de referencia.

Cuando el cuerpo del destinatario no está disponible en la unidad de traducción (la función reside en una biblioteca compartida o en otra unidad de traducción y la optimización de tiempo de enlace no está disponible), suceden las siguientes cosas:

  1. El optimizador asume que los argumentos pasados ​​por referencia o referencia a const pueden ser cambiados ( const no importa debido a const_cast ) o pueden ser referidos por un puntero global, o cambiados por otro hilo. Básicamente, los argumentos que se pasan por referencia se convierten en valores "envenenados" en el sitio de la llamada, en los que el optimizador no puede aplicar muchas optimizaciones.
  2. En el destinatario de la llamada, si hay varios argumentos de referencia / puntero del mismo tipo de base, el optimizador asume que son alias con otra cosa y eso nuevamente excluye muchas optimizaciones.

Desde el punto de vista del optimizador, lo mejor es pasar y volver por valor, ya que esto evita la necesidad de un análisis de alias: el que llama y el que llama tienen sus copias de los valores exclusivamente para que estos valores no puedan modificarse desde ningún otro lugar.

Para un tratamiento detallado del tema, no puedo recomendar lo suficiente Chandler Carruth: optimizar las estructuras emergentes de C ++ . La clave de la charla es "la gente necesita cambiar de opinión acerca de pasar por valor ... el modelo de registro de los argumentos de aprobación es obsoleto".


Aquí están mis reglas generales para pasar variables a funciones:

  1. Si la variable puede caber dentro del registro del procesador y no se modificará, pase por valor.
  2. Si la variable será modificada, pase por referencia.
  3. Si la variable es más grande que el registro del procesador y no se modificará, pase por referencia constante.
  4. Si necesita utilizar punteros, pase el puntero inteligente.

Espero que ayude.


Dejando a un lado las preguntas filosóficas sobre el valor de señalización de la constancia y el valor como parámetros de función, podemos echar un vistazo a algunas implicaciones ABI en varias arquitecturas.

http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ presenta algunas decisiones y pruebas realizadas por algunas personas de QT en x86-64, ARMv7 hard-float, MIPS hard-float (o32) y IA-64. Sobre todo, verifica si las funciones pueden pasar varias estructuras a través de registros. No en vano, parece que cada plataforma puede gestionar 2 punteros por registro. Y dado que sizeof (size_t) es generalmente sizeof (void *), hay pocas razones para creer que estaremos en la memoria aquí.

Podemos encontrar más madera para el fuego, considerando sugerencias como: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html . Tenga en cuenta que la referencia constante tiene algunas desventajas, a saber, el riesgo de alias, que puede evitar optimizaciones importantes y requiere una reflexión adicional para el programador. En ausencia de soporte de C ++ para la restricción de C99, pasar por valor puede mejorar el rendimiento y disminuir la carga cognitiva.

Supongo entonces que estoy sintetizando dos argumentos a favor de pasar por valor:

  1. Las plataformas de 32 bits a menudo carecían de la capacidad para pasar estructuras de dos palabras por registro. Esto ya no parece ser un problema.
  2. Las referencias const son cuantitativamente y cualitativamente peores que los valores en que pueden ser alias.

Todo lo cual me llevaría a favorecer el paso por valor para estructuras de <16 bytes de tipos integrales. Obviamente, su millaje puede variar y las pruebas siempre deben realizarse cuando el rendimiento es un problema, pero los valores parecen un poco más agradables para los tipos muy pequeños.


EDITAR: El código está disponible aquí: https://github.com/acmorrow/stringview_param

He creado un código de ejemplo que parece demostrar que el paso por valor para string_view objetos semejantes da como resultado un mejor código tanto para las personas que llaman como para las definiciones de funciones en al menos una plataforma .

Primero, definimos una clase de string_view falsa (no tenía la cosa real a mano) en string_view.h :

#pragma once #include <string> class string_view { public: string_view() : _data(nullptr) , _len(0) { } string_view(const char* data) : _data(data) , _len(strlen(data)) { } string_view(const std::string& data) : _data(data.data()) , _len(data.length()) { } const char* data() const { return _data; } std::size_t len() const { return _len; } private: const char* _data; size_t _len; };

Ahora, definamos algunas funciones que consumen una string_view, ya sea por valor o por referencia. Aquí están las firmas en example.hpp :

#pragma once class string_view; void __attribute__((visibility("default"))) use_as_value(string_view view); void __attribute__((visibility("default"))) use_as_const_ref(const string_view& view);

Los cuerpos de estas funciones se definen como sigue, en example.cpp :

#include "example.hpp" #include <cstdio> #include "do_something_else.hpp" #include "string_view.hpp" void use_as_value(string_view view) { printf("%ld %ld %zu/n", strchr(view.data(), ''a'') - view.data(), view.len(), strlen(view.data())); do_something_else(); printf("%ld %ld %zu/n", strchr(view.data(), ''a'') - view.data(), view.len(), strlen(view.data())); } void use_as_const_ref(const string_view& view) { printf("%ld %ld %zu/n", strchr(view.data(), ''a'') - view.data(), view.len(), strlen(view.data())); do_something_else(); printf("%ld %ld %zu/n", strchr(view.data(), ''a'') - view.data(), view.len(), strlen(view.data())); }

La función do_something_else está aquí es un soporte para llamadas arbitrarias a funciones sobre las que el compilador no tiene conocimiento (por ejemplo, funciones de otros objetos dinámicos, etc.). La declaración está en do_something_else.hpp :

#pragma once void __attribute__((visibility("default"))) do_something_else();

Y la definición trivial está en do_something_else.cpp :

#include "do_something_else.hpp" #include <cstdio> void do_something_else() { std::printf("Doing something/n"); }

Ahora compilamos do_something_else.cpp y example.cpp en bibliotecas dinámicas individuales. El compilador aquí es XCode 6 clang en OS X Yosemite 10.10.1:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./do_something_else.cpp -fPIC -shared -o libdo_something_else.dylib clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example.cpp -fPIC -shared -o libexample.dylib -L. -ldo_something_else

Ahora, desmontamos libexample.dylib:

> otool -tVq ./libexample.dylib ./libexample.dylib: (__TEXT,__text) section __Z12use_as_value11string_view: 0000000000000d80 pushq %rbp 0000000000000d81 movq %rsp, %rbp 0000000000000d84 pushq %r15 0000000000000d86 pushq %r14 0000000000000d88 pushq %r12 0000000000000d8a pushq %rbx 0000000000000d8b movq %rsi, %r14 0000000000000d8e movq %rdi, %rbx 0000000000000d91 movl $0x61, %esi 0000000000000d96 callq 0xf42 ## symbol stub for: _strchr 0000000000000d9b movq %rax, %r15 0000000000000d9e subq %rbx, %r15 0000000000000da1 movq %rbx, %rdi 0000000000000da4 callq 0xf48 ## symbol stub for: _strlen 0000000000000da9 movq %rax, %rcx 0000000000000dac leaq 0x1d5(%rip), %r12 ## literal pool for: "%ld %ld %zu/n" 0000000000000db3 xorl %eax, %eax 0000000000000db5 movq %r12, %rdi 0000000000000db8 movq %r15, %rsi 0000000000000dbb movq %r14, %rdx 0000000000000dbe callq 0xf3c ## symbol stub for: _printf 0000000000000dc3 callq 0xf36 ## symbol stub for: __Z17do_something_elsev 0000000000000dc8 movl $0x61, %esi 0000000000000dcd movq %rbx, %rdi 0000000000000dd0 callq 0xf42 ## symbol stub for: _strchr 0000000000000dd5 movq %rax, %r15 0000000000000dd8 subq %rbx, %r15 0000000000000ddb movq %rbx, %rdi 0000000000000dde callq 0xf48 ## symbol stub for: _strlen 0000000000000de3 movq %rax, %rcx 0000000000000de6 xorl %eax, %eax 0000000000000de8 movq %r12, %rdi 0000000000000deb movq %r15, %rsi 0000000000000dee movq %r14, %rdx 0000000000000df1 popq %rbx 0000000000000df2 popq %r12 0000000000000df4 popq %r14 0000000000000df6 popq %r15 0000000000000df8 popq %rbp 0000000000000df9 jmp 0xf3c ## symbol stub for: _printf 0000000000000dfe nop __Z16use_as_const_refRK11string_view: 0000000000000e00 pushq %rbp 0000000000000e01 movq %rsp, %rbp 0000000000000e04 pushq %r15 0000000000000e06 pushq %r14 0000000000000e08 pushq %r13 0000000000000e0a pushq %r12 0000000000000e0c pushq %rbx 0000000000000e0d pushq %rax 0000000000000e0e movq %rdi, %r14 0000000000000e11 movq (%r14), %rbx 0000000000000e14 movl $0x61, %esi 0000000000000e19 movq %rbx, %rdi 0000000000000e1c callq 0xf42 ## symbol stub for: _strchr 0000000000000e21 movq %rax, %r15 0000000000000e24 subq %rbx, %r15 0000000000000e27 movq 0x8(%r14), %r12 0000000000000e2b movq %rbx, %rdi 0000000000000e2e callq 0xf48 ## symbol stub for: _strlen 0000000000000e33 movq %rax, %rcx 0000000000000e36 leaq 0x14b(%rip), %r13 ## literal pool for: "%ld %ld %zu/n" 0000000000000e3d xorl %eax, %eax 0000000000000e3f movq %r13, %rdi 0000000000000e42 movq %r15, %rsi 0000000000000e45 movq %r12, %rdx 0000000000000e48 callq 0xf3c ## symbol stub for: _printf 0000000000000e4d callq 0xf36 ## symbol stub for: __Z17do_something_elsev 0000000000000e52 movq (%r14), %rbx 0000000000000e55 movl $0x61, %esi 0000000000000e5a movq %rbx, %rdi 0000000000000e5d callq 0xf42 ## symbol stub for: _strchr 0000000000000e62 movq %rax, %r15 0000000000000e65 subq %rbx, %r15 0000000000000e68 movq 0x8(%r14), %r14 0000000000000e6c movq %rbx, %rdi 0000000000000e6f callq 0xf48 ## symbol stub for: _strlen 0000000000000e74 movq %rax, %rcx 0000000000000e77 xorl %eax, %eax 0000000000000e79 movq %r13, %rdi 0000000000000e7c movq %r15, %rsi 0000000000000e7f movq %r14, %rdx 0000000000000e82 addq $0x8, %rsp 0000000000000e86 popq %rbx 0000000000000e87 popq %r12 0000000000000e89 popq %r13 0000000000000e8b popq %r14 0000000000000e8d popq %r15 0000000000000e8f popq %rbp 0000000000000e90 jmp 0xf3c ## symbol stub for: _printf 0000000000000e95 nopw %cs:(%rax,%rax)

Curiosamente, la versión por valor es varias instrucciones más cortas. Pero eso es solo la función de los cuerpos. ¿Qué pasa con las personas que llaman?

Definiremos algunas funciones que invocan estas dos sobrecargas, reenviando un const std::string& , en example_users.hpp :

#pragma once #include <string> void __attribute__((visibility("default"))) forward_to_use_as_value(const std::string& str); void __attribute__((visibility("default"))) forward_to_use_as_const_ref(const std::string& str);

Y example_users.cpp en example_users.cpp :

#include "example_users.hpp" #include "example.hpp" #include "string_view.hpp" void forward_to_use_as_value(const std::string& str) { use_as_value(str); } void forward_to_use_as_const_ref(const std::string& str) { use_as_const_ref(str); }

De nuevo, example_users.cpp en una biblioteca compartida:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example_users.cpp -fPIC -shared -o libexample_users.dylib -L. -lexample

Y, de nuevo, nos fijamos en el código generado:

> otool -tVq ./libexample_users.dylib ./libexample_users.dylib: (__TEXT,__text) section __Z23forward_to_use_as_valueRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: 0000000000000e70 pushq %rbp 0000000000000e71 movq %rsp, %rbp 0000000000000e74 movzbl (%rdi), %esi 0000000000000e77 testb $0x1, %sil 0000000000000e7b je 0xe8b 0000000000000e7d movq 0x8(%rdi), %rsi 0000000000000e81 movq 0x10(%rdi), %rdi 0000000000000e85 popq %rbp 0000000000000e86 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view 0000000000000e8b incq %rdi 0000000000000e8e shrq %rsi 0000000000000e91 popq %rbp 0000000000000e92 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view 0000000000000e97 nopw (%rax,%rax) __Z27forward_to_use_as_const_refRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: 0000000000000ea0 pushq %rbp 0000000000000ea1 movq %rsp, %rbp 0000000000000ea4 subq $0x10, %rsp 0000000000000ea8 movzbl (%rdi), %eax 0000000000000eab testb $0x1, %al 0000000000000ead je 0xebd 0000000000000eaf movq 0x10(%rdi), %rax 0000000000000eb3 movq %rax, -0x10(%rbp) 0000000000000eb7 movq 0x8(%rdi), %rax 0000000000000ebb jmp 0xec7 0000000000000ebd incq %rdi 0000000000000ec0 movq %rdi, -0x10(%rbp) 0000000000000ec4 shrq %rax 0000000000000ec7 movq %rax, -0x8(%rbp) 0000000000000ecb leaq -0x10(%rbp), %rdi 0000000000000ecf callq 0xf66 ## symbol stub for: __Z16use_as_const_refRK11string_view 0000000000000ed4 addq $0x10, %rsp 0000000000000ed8 popq %rbp 0000000000000ed9 retq 0000000000000eda nopw (%rax,%rax)

Y, de nuevo, la versión por valor es varias instrucciones más cortas.

Me parece que, al menos por la métrica gruesa del recuento de instrucciones, la versión en valor produce un mejor código tanto para los llamadores como para los cuerpos de función generados.

Por supuesto, estoy abierto a sugerencias sobre cómo mejorar esta prueba. Obviamente, un próximo paso sería refactorizar esto en algo donde pudiera evaluarlo de manera significativa. Intentaré hacerlo pronto.

Pondré el código de ejemplo en github con algún tipo de script de compilación para que otros puedan probar en sus sistemas.

Pero en base a la discusión anterior y los resultados de la inspección del código generado, mi conclusión es que el paso por valor es el camino a seguir para los tipos de vista.


En caso de duda, pasar por valor.

Ahora, rara vez deberías estar en duda.

A menudo los valores son caros para pasar y dan poco beneficio. A veces realmente desea una referencia a un posible valor de mutación almacenado en otra parte A menudo, en el código genérico, no sabes si copiar es una operación costosa, por lo que te equivocas en el lado de no.

La razón por la que debería pasar por valor cuando tenga dudas es porque los valores son más fáciles de razonar. Una referencia (incluso una const ) a los datos externos podría mutar en medio de un algoritmo cuando llama a una función de devolución de llamada o lo que sea, convirtiendo lo que parece ser una función simple en un desorden complejo.

En este caso, ya tiene un enlace de referencia implícito (al contenido del contenedor que está viendo). Agregar otro enlace de referencia implícito (al objeto de vista que se ve en el contenedor) no es menos malo porque ya hay complicaciones.

Finalmente, los compiladores pueden razonar sobre valores mejor que sobre referencias a valores. Si deja el ámbito analizado localmente (a través de una función de devolución de llamada de puntero), el compilador debe suponer que el valor almacenado en la referencia de const puede haber cambiado completamente (si no puede demostrar lo contrario). Se puede suponer que un valor en el almacenamiento automático sin un puntero hacia él no se modifica de manera similar: no hay una forma definida de acceder a él y cambiarlo desde un ámbito externo, por lo que se puede presumir que tales modificaciones no ocurren. .

Adopte la simplicidad cuando tenga la oportunidad de pasar un valor como un valor. Sólo sucede raramente.


Mi argumento sería utilizar ambos. Prefiero const y. También llega a ser documentación también. Si lo ha declarado como const y, entonces el compilador se quejará si intenta modificar la instancia (cuando no tenía la intención de hacerlo). Si pretende modificarlo, tómelo por valor. Pero de esta manera, se está comunicando explícitamente a los futuros desarrolladores con la intención de modificar la instancia. Y const y es "probablemente no peor" que por valor, y potencialmente mucho mejor (si construir una instancia es costoso y no tiene una).


Un valor es un valor y una referencia constante es una referencia constante.

Si el objeto no es inmutable, entonces los dos NO son conceptos equivalentes.

Sí ... incluso un objeto recibido a través de una referencia const puede mutar (o incluso puede ser destruido mientras todavía tienes una referencia constante en tus manos). const con una referencia solo dice lo que se puede hacer usando esa referencia , no dice nada acerca de que el objeto al que se hace referencia no muta o no deja de existir por otros medios.

Para ver un caso muy simple en el que el alias puede morder mal con un código aparentemente legítimo, vea esta respuesta .

Debe usar una referencia donde la lógica requiera una referencia (es decir, la identidad del objeto es importante). Debe pasar un valor cuando la lógica requiera solo el valor (es decir, la identidad del objeto es irrelevante). Con los inmutables normalmente la identidad es irrelevante.

Cuando use una referencia, debe prestarse atención especial a los problemas de alias y de por vida. Por otro lado, al pasar valores, debe considerar que la copia está posiblemente involucrada, por lo tanto, si la clase es grande y esto probablemente sea un grave cuello de botella para su programa, entonces puede considerar pasar una referencia constante en su lugar (y volver a verificar el alias y los problemas de por vida) .

En mi opinión, en este caso específico (solo un par de tipos nativos), la excusa para necesitar la eficiencia de aprobación constante de referencia sería bastante difícil de justificar. Lo más probable es que, de todas formas, todo esté en línea y las referencias solo dificultarán la optimización de las cosas.

Especificar un parámetro const T& cuando el destinatario no está interesado en la identidad (es decir, los cambios de estado futuros * ) es un error de diseño. La única justificación para cometer este error intencionalmente es cuando el objeto es pesado y hacer una copia es un problema grave de rendimiento.

Para los objetos pequeños, hacer copias a menudo es mejor desde el punto de vista del rendimiento porque hay una indirección menos y el lado paranoico del optimizador no necesita considerar los problemas de aliasing. Por ejemplo, si tiene F(const X& a, Y& b) y X contiene un miembro de tipo Y el optimizador se verá obligado a considerar la posibilidad de que la referencia no constante esté realmente vinculada a ese subobjeto de X

(*) Con "futuro", lo estoy incluyendo después de regresar del método (es decir, el destinatario almacena la dirección del objeto y lo recuerda) y durante la ejecución del código del destinatario (es decir, alias).


Ya que no hace la menor diferencia cuál usas en este caso, esto parece ser solo un debate sobre egos. Esto no es algo que deba contener una revisión de código. A menos que alguien mida el rendimiento y descubra que este código es crítico en el tiempo, lo cual dudo mucho.