¿Diferencia entre std :: reference_wrapper y puntero simple?

99

¿Por qué es necesario tenerlo std::reference_wrapper? ¿Dónde debe usarse? ¿En qué se diferencia de un simple puntero? ¿Cómo se compara su rendimiento con un simple puntero?

Laurynas Lazauskas
fuente
4
Básicamente es un puntero que usa .en lugar de->
MM
5
@MM No, el uso .no funciona de la manera que usted sugiere que lo hace (a menos que en algún momento se adopte e integre la propuesta de punto del operador :))
Columbo
3
Son preguntas como esta las que me hacen infeliz cuando tengo que trabajar con el nuevo C ++.
Nils
Para seguir a Columbo, std :: reference_wrapper se usa con su get()función miembro o con su conversión implícita de nuevo al tipo subyacente.
Max Barraclough

Respuestas:

88

std::reference_wrapperes útil en combinación con plantillas. Envuelve un objeto almacenando un puntero hacia él, lo que permite la reasignación y la copia mientras imita su semántica habitual. También indica a ciertas plantillas de biblioteca que almacenen referencias en lugar de objetos.

Considere los algoritmos en el STL que copian functores: puede evitar esa copia simplemente pasando un contenedor de referencia que se refiera al functor en lugar del functor en sí:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

Esto funciona porque ...

  • ... reference_wrappers sobrecargaoperator() para que se puedan llamar como los objetos de función a los que se refieren:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Des) me gustan las referencias ordinarias, copiar (y asignar) reference_wrapperssimplemente asigna el puntero.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>
    

Copiar un contenedor de referencia es prácticamente equivalente a copiar un puntero, que es lo más económico posible. Todas las llamadas a funciones inherentes a su uso (por ejemplo, las que operator()deben) deben estar en línea, ya que son una línea.

reference_wrapperLos mensajes de correo electrónico se crean mediante std::refystd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

El argumento de plantilla especifica el tipo y la calificación cv del objeto al que se hace referencia; r2se refiere a const intay solo dará una referencia a const int. Las llamadas a envoltorios de referencia con constfunctores en ellos solo llamarán a constfunciones miembro operator().

Los inicializadores Rvalue no están permitidos, ya que permitirlos haría más daño que bien. Dado que los rvalues ​​se moverían de todos modos (y con la elisión de copia garantizada incluso eso se evita en parte), no mejoramos la semántica; Sin embargo, podemos introducir punteros colgantes, ya que una envoltura de referencia no extiende la vida útil del puntero.

Interacción con la biblioteca

Como se mencionó anteriormente, uno puede instruir make_tuplepara almacenar una referencia en el resultado tuplepasando el argumento correspondiente a través de un reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

Tenga en cuenta que esto difiere ligeramente de forward_as_tuple: Aquí, no se permiten valores r como argumentos.

std::bindmuestra el mismo comportamiento: no copiará el argumento, pero almacenará una referencia si es un reference_wrapper. Útil si ese argumento (¡o el functor!) No necesita copiarse pero permanece dentro del alcance mientras bindse usa el -functor.

Diferencia de punteros ordinarios

  • No hay un nivel adicional de indirección sintáctica. Los punteros deben desreferenciarse para obtener un valor l del objeto al que se refieren; reference_wrappers tienen un operador de conversión implícito y se pueden llamar como el objeto que envuelven.

    int i;
    int& ref = std::ref(i); // Okay
    
  • reference_wrappers, a diferencia de los punteros, no tienen un estado nulo. Deben inicializarse con una referencia u otrareference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Una similitud es la semántica de copia superficial: los punteros y reference_wrappers pueden reasignarse.

Columbo
fuente
¿Es std::make_tuple(std::ref(i));superior a std::make_tuple(&i);de alguna manera?
Laurynas Lazauskas
6
@LaurynasLazauskas Es diferente. El último que mostró guarda un puntero i, no una referencia a él.
Columbo
Hm ... creo que todavía no puedo distinguir estos dos tan bien como me gustaría ... Bueno, gracias.
Laurynas Lazauskas
@Columbo ¿Cómo es posible una matriz de envoltorios de referencia si no tienen un estado nulo? ¿Las matrices generalmente no comienzan con todos los elementos configurados en estado nulo?
anatolyg
2
@anatolyg ¿Qué le impide inicializar esa matriz?
Columbo
27

Hay, al menos, dos propósitos motivadores de std::reference_wrapper<T>:

  1. Es para dar semántica de referencia a los objetos pasados ​​como parámetro de valor a las plantillas de funciones. Por ejemplo, puede tener un objeto de función grande que desee pasar al std::for_each()que toma su parámetro de objeto de función por valor. Para evitar copiar el objeto, puede utilizar

    std::for_each(begin, end, std::ref(fun));

    Pasar argumentos como std::reference_wrapper<T>una std::bind()expresión es bastante común para vincular argumentos por referencia en lugar de por valor.

  2. Cuando se usa un std::reference_wrapper<T>con std::make_tuple()el elemento de tupla correspondiente se convierte en un en T&lugar de un T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
    
Dietmar Kühl
fuente
¿Puede dar un ejemplo de código para el primer caso?
user1708860
1
@ user1708860: ¿te refieres a otro que no sea el dado ...?
Dietmar Kühl
Me refiero al código real que va con std :: ref (fun) porque no entiendo cómo se usa (a menos que la diversión sea un objeto y no una función ...)
user1708860
2
@ user1708860: sí, lo más probable funes que sea ​​un objeto de función (es decir, un objeto de una clase con un operador de llamada de función) y no una función: si funresulta ser una función real, std::ref(fun)no tiene ningún propósito y hace que el código sea potencialmente más lento.
Dietmar Kühl
23

Otra diferencia, en términos de código autodocumentado, es que usar un reference_wrapperesencialmente desautoriza la propiedad del objeto. Por el contrario, una unique_ptrafirma la propiedad, mientras que un puntero simple puede o no ser propiedad (no es posible saberlo sin mirar mucho código relacionado):

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned
Edward Loper
fuente
3
a menos que sea un código anterior a c ++ 11, el primer ejemplo debe implicar valores opcionales sin propietario, por ejemplo, para una búsqueda de caché basada en index. Sería bueno si std nos proporcionara algo estándar para representar un valor de propiedad no nulo (variantes únicas y compartidas)
Bwmat
Quizás no sea tan importante en C ++ 11, donde los punteros simples casi siempre serán valores prestados de todos modos.
Elling
reference_wrapperes superior a los punteros en bruto no solo porque está claro que no es propietario, sino también porque no puede ser nullptr(sin travesuras) y, por lo tanto, los usuarios saben que no pueden aprobar nullptr(sin travesuras) y usted sabe que no tiene que hacerlo. comprobarlo.
underscore_d
19

Puede considerarlo como un envoltorio conveniente para las referencias para que pueda usarlas en contenedores.

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

Básicamente es una CopyAssignableversión de T&. Siempre que desee una referencia, pero tiene que ser asignable, use std::reference_wrapper<T>o su función de ayuda std::ref(). O usa un puntero.


Otras peculiaridades sizeof::

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

Y comparación:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error
Barry
fuente
1
@LaurynasLazauskas Uno podría llamar directamente a los objetos de función contenidos en el contenedor. Eso también se explica en mi respuesta.
Columbo
2
Dado que la implementación de referencia es solo un puntero en el interior, no puedo entender por qué los contenedores agregan direccionamiento indirecto o penalización de rendimiento
Riga
4
No debería ser más indirecto que una simple referencia cuando se trata de un código de lanzamiento
Riga
3
Esperaría que el compilador insertara el reference_wrappercódigo trivial , haciéndolo idéntico al código que usa un puntero o referencia.
David Stone
4
@LaurynasLazauskas: std::reference_wrappertiene la garantía de que el objeto nunca es nulo. Piense en un miembro de la clase std::vector<T *>. Debe examinar todo el código de la clase para ver si este objeto alguna vez puede almacenar un nullptren el vector, mientras que con std::reference_wrapper<T>, se garantiza que tendrá objetos válidos.
David Stone