valor referencia punteros por poner paso parametros llamadas ejercicios como c++ c++11 one-definition-rule forwarding-reference pass-by-const-reference

punteros - paso por valor y por referencia c++



Pasando el literal como un parámetro de referencia constante (3)

Imagina el siguiente código simplificado:

#include <iostream> void foo(const int& x) { do_something_with(x); } int main() { foo(42); return 0; }

(1) Dejando de lado las optimizaciones, ¿qué sucede cuando se pasan 42 a foo ?

¿El compilador se pega al 42 en algún lugar (en la pila) y le pasa su dirección a foo ?

(1a) ¿Hay algo en la norma que dicta lo que se debe hacer en esta situación (o depende exclusivamente del compilador)?

Ahora, imagina un código ligeramente diferente:

#include <iostream> void foo(const int& x) { do_something_with(x); } struct bar { static constexpr int baz = 42; }; int main() { foo(bar::baz); return 0; }

No enlazará, a menos que defina int bar::baz; (¿Debido a la ODR?).

(2) Además de la ODR, ¿por qué el compilador no puede hacer lo que hizo con 42 arriba?

Una forma obvia de simplificar las cosas es definir foo como:

void foo(int x) { do_something_with(x); }

Sin embargo, ¿qué haría uno en caso de una plantilla? P.ej:

template<typename T> void foo(T&& x) { do_something_with(std::forward<T>(x)); }

(3) ¿Existe una manera elegante de decirle a foo que acepte x por valor para tipos primitivos? ¿O necesito especializarme con SFINAE o algo así?

EDIT : se modificó lo que sucede dentro de foo ya que es irrelevante para esta pregunta.


¿El compilador se pega al 42 en algún lugar (en la pila) y le pasa su dirección a foo ?

Se crea un objeto temporal de tipo const int , se inicializa con la expresión prvalue 42 y se enlaza a la referencia.

En la práctica, si foo no está en línea, eso requiere asignar espacio en la pila, almacenar 42 en ella y pasar la dirección.

¿Hay algo en el estándar que dicta lo que se debe hacer en esta situación (o es estrictamente para el compilador)?

[dcl.init.ref] .

Además de la ODR, ¿por qué el compilador no puede hacer lo que hizo con 42 arriba?

Debido a que de acuerdo con el lenguaje, la referencia está vinculada a la bar::baz objetos bar::baz , y, a menos que el compilador sepa exactamente qué está haciendo foo en el punto en el que está compilando la llamada, debe asumir que esto es importante. Por ejemplo, si foo contiene un assert(&x == &bar::baz); , que no debe disparar con foo(bar::baz) .

(En C ++ 17, baz está implícitamente en línea como un miembro de datos estáticos constexpr ; no se requiere una definición separada).

¿Hay una manera elegante de decirle a foo que acepte x por valor para tipos primitivos?

Por lo general, no tiene mucho sentido hacer esto en ausencia de datos de perfiles que muestren que el paso por referencia en realidad está causando problemas, pero si realmente necesita hacerlo por algún motivo, agregar sobrecargas (posiblemente con restricciones de SFINAE) sería la mejor opción. camino a seguir.


Con C ++ 17, el código compila perfectamente considerando el uso de bar :: baz como en línea, con C ++ 14 la plantilla requiere prvalue como argumento, por lo que el compilador conserva un símbolo para bar::baz en el código objeto. Lo cual no se resolverá porque no tenías esa declaración. constexpr debe ser tratado como constprvalue o rvalues ​​por compilador, en la generación de código que puede llevar a un enfoque diferente. Por ejemplo, si la función llamada está en línea, el compilador puede generar código que usa ese valor particular como argumento constante de la instrucción del procesador. Las palabras clave aquí son "debería ser" y "puede", que son tan diferentes de "debe" como la cláusula de exención de responsabilidad habitual en los estados de documentación estándar general.

Para un tipo primitivo, para un valor temporal y constexpr no habrá diferencia, en qué firma de plantilla utiliza. La forma en que el compilador realmente lo implementa, depende de la plataforma y del compilador ... y de las convenciones de llamada utilizadas. realmente no podemos saber si algo está en la pila con seguridad, porque algunas plataformas NO tienen pila o se implementa de manera diferente a la pila en la plataforma x86. Múltiples convenciones de llamadas modernas utilizan registros de CPU para pasar argumentos.

Si su compilador es lo suficientemente moderno, no necesita referencias en absoluto, copiar elision le ahorraría operaciones de copia adicionales. Para probar que:

#include <iostream> template<typename T> void foo(T x) { std::cout << x.baz << std::endl; } #include <iostream> using namespace std; struct bar { int baz; bar(const int b = 0): baz(b) { cout << "Constructor called" << endl; } bar(const bar &b): baz(b.baz) //copy constructor { cout << "Copy constructor called" << endl; } }; int main() { foo(bar(42)); }

resultará en salida:

Constructor called 42

Pasar por referencia, por una referencia constante no costaría más que pasar por valor, especialmente para las plantillas. Si necesita una semántica diferente, necesitará una especialización explícita de la plantilla. Algunos compiladores más antiguos no podían soportar a estos últimos de manera adecuada.

template<typename T> void foo(const T& x) { std::cout << x.baz << std::endl; } // ... bar b(42); foo(b);

Salida:

Constructor called 42

La referencia no constante no nos permitiría reenviar argumentos, si se tratara de un valor l, por ejemplo

template<typename T> void foo(T& x) { std::cout << x.baz << std::endl; } // ... foo(bar(42));

llamando a esta plantilla (llamada reenvío perfecto)

template<typename T> void foo(T&& x) { std::cout << x << std::endl; }

uno podría evitar los problemas de reenvío, aunque este proceso también implicaría la resolución de la copia. El compilador deduce el parámetro de plantilla como sigue de C ++ 17

template <class T> int f(T&& heisenreference); template <class T> int g(const T&&); int i; int n1 = f(i); // calls f<int&>(int&) int n2 = f(0); // calls f<int>(int&&) int n3 = g(i); // error: would call g<int>(const int&&), which // would bind an rvalue reference to an lvalue

Una referencia de reenvío es una referencia rvalue a un parámetro de plantilla no calificada cv. Si P es una referencia de reenvío y el argumento es un valor l, se usa el tipo "referencia a valor l" a en lugar de A para la deducción de tipo.


Tu ejemplo # 1 . La ubicación constante depende completamente del compilador y no está definida en un estándar. GCC en Linux podría asignar tales constantes en una sección de memoria de solo lectura estática. La optimización probablemente lo eliminará todo junto.

Su ejemplo # 2 no se compilará (antes del enlace). Debido a las reglas de alcance. Así que necesitas bar::baz allí.

ejemplo # 3, normalmente hago esto:

template<typename T> void foo(const T& x) { std::cout << x << std::endl; }