Différence entre std :: reference_wrapper et simple pointeur?

100

Pourquoi est-il nécessaire d'avoir std::reference_wrapper? Où doit-il être utilisé? En quoi est-ce différent d'un simple pointeur? Comment ses performances se comparent-elles à un simple pointeur?

Laurynas Lazauskas
la source
4
C'est essentiellement un pointeur que vous utilisez .avec au lieu de->
MM
5
@MM Non, l'utilisation .ne fonctionne pas comme vous le suggérez (à moins qu'à un moment donné, la proposition de point de l'opérateur ne soit adoptée et intégrée :))
Columbo
3
Ce sont des questions comme celle-ci qui me rendent malheureux quand je dois travailler avec le nouveau C ++.
Nils
Pour suivre Columbo, std :: reference_wrapper est utilisé avec sa get()fonction membre ou avec sa conversion implicite vers le type sous-jacent.
Max Barraclough

Réponses:

88

std::reference_wrapperest utile en combinaison avec des modèles. Il enveloppe un objet en stockant un pointeur vers lui, ce qui permet la réaffectation et la copie tout en imitant sa sémantique habituelle. Il indique également à certains modèles de bibliothèque de stocker des références au lieu d'objets.

Considérez les algorithmes de la STL qui copient les foncteurs: Vous pouvez éviter cette copie en passant simplement un wrapper de référence faisant référence au foncteur au lieu du foncteur lui-même:

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

Cela fonctionne parce que…

  • reference_wrapperS surchargeoperator() afin qu'ils puissent être appelés comme les objets de fonction auxquels ils font référence:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Un) comme les références ordinaires, copier (et assigner) reference_wrappersne fait qu'attribuer la pointee.

    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>
    

Copier un wrapper de référence équivaut pratiquement à copier un pointeur, ce qui est aussi bon marché que possible. Tous les appels de fonction inhérents à son utilisation (par exemple ceux à operator()) doivent être simplement insérés car ce sont des one-liners.

reference_wrappers sont créés via std::refetstd::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>

L'argument template spécifie le type et la qualification cv de l'objet référencé; r2fait référence à a const intet ne donnera qu'une référence à const int. Les appels à des wrappers de référence contenant des constfoncteurs n'appelleront que des constfonctions membres operator().

Les initialiseurs Rvalue sont interdits, car les autoriser ferait plus de mal que de bien. Puisque les rvalues ​​seraient déplacées de toute façon (et avec une élision de copie garantie même si cela est évité en partie), nous n'améliorons pas la sémantique; nous pouvons cependant introduire des pointeurs pendants, car un wrapper de référence ne prolonge pas la durée de vie du pointeur.

Interaction avec la bibliothèque

Comme mentionné précédemment, on peut ordonner make_tuplede stocker une référence dans le résultat tupleen passant l'argument correspondant via 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&>

Notez que ceci diffère légèrement de forward_as_tuple: Ici, les rvalues ​​comme arguments ne sont pas autorisées.

std::bindmontre le même comportement: il ne copiera pas l'argument mais stockera une référence s'il s'agit d'un reference_wrapper. Utile si cet argument (ou le foncteur!) bindN'a pas besoin d'être copié mais reste dans la portée pendant que le -foncteur est utilisé.

Différence par rapport aux pointeurs ordinaires

  • Il n'y a pas de niveau supplémentaire d'indirection syntaxique. Les pointeurs doivent être déréférencés pour obtenir une lvaleur vers l'objet auquel ils font référence; reference_wrappers ont un opérateur de conversion implicite et peuvent être appelés comme l'objet qu'ils encapsulent.

    int i;
    int& ref = std::ref(i); // Okay
    
  • reference_wrappers, contrairement aux pointeurs, n'ont pas d'état nul. Ils doivent être initialisés avec une référence ou une autrereference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Une similitude est la sémantique de copie superficielle: les pointeurs et les reference_wrappers peuvent être réaffectés.

Columbo
la source
Est-ce std::make_tuple(std::ref(i));supérieur d'une std::make_tuple(&i);certaine manière?
Laurynas Lazauskas
6
@LaurynasLazauskas C'est différent. Ce dernier que vous avez montré enregistre un pointeur vers i, pas une référence à celui-ci.
Columbo
Hm ... Je suppose que je ne peux toujours pas distinguer ces deux aussi bien que je le voudrais ... Eh bien, merci.
Laurynas Lazauskas
@Columbo Comment un tableau de wrappers de référence est-il possible s'ils n'ont pas l'état nul? Les tableaux ne commencent-ils généralement pas avec tous les éléments définis à l'état nul?
anatolyg
2
@anatolyg Qu'est-ce qui vous empêche d'initialiser ce tableau?
Columbo
27

Il y a au moins deux objectifs motivants std::reference_wrapper<T>:

  1. Il s'agit de donner une sémantique de référence aux objets passés en paramètre de valeur aux modèles de fonctions. Par exemple, vous pouvez avoir un objet fonction volumineux que vous souhaitez transmettre et std::for_each()qui prend son paramètre d'objet fonction par valeur. Pour éviter de copier l'objet, vous pouvez utiliser

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

    Passer des arguments std::reference_wrapper<T>à une std::bind()expression est assez courant pour lier des arguments par référence plutôt que par valeur.

  2. Lorsque vous utilisez un std::reference_wrapper<T>avec std::make_tuple()l'élément de tuple correspondant, il devient un T&plutôt qu'un T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
Dietmar Kühl
la source
Pouvez-vous s'il vous plaît donner un exemple de code pour le premier cas?
user1708860
1
@ user1708860: vous voulez dire autre que celui donné ...?
Dietmar Kühl
Je veux dire le code réel qui va avec le std :: ref (fun) parce que je ne comprends pas comment est utilisé (sauf si le plaisir est un objet et non une fonction ...)
user1708860
2
@ user1708860: oui, le plus probable funest un objet fonction (c'est-à-dire un objet d'une classe avec un opérateur d'appel de fonction) et non une fonction: s'il funse trouve être une fonction réelle, cela std::ref(fun)n'a aucun but et rend le code potentiellement plus lent.
Dietmar Kühl
23

Une autre différence, en termes de code auto-documenté, est que l'utilisation d'un reference_wrapperdésavoue essentiellement la propriété de l'objet. En revanche, un unique_ptrrevendique la propriété, tandis qu'un pointeur nu peut ou non être possédé (il n'est pas possible de le savoir sans regarder beaucoup de code associé):

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
la source
3
à moins qu'il ne s'agisse de code pré-C ++ 11, le premier exemple devrait impliquer des valeurs facultatives et non propriétaires, par exemple pour une recherche de cache basée sur un index. Ce serait bien si std nous fournissait quelque chose de standard pour représenter une valeur détenue non nulle (variantes uniques et partagées)
Bwmat
Ce n'est peut-être pas aussi important dans C ++ 11, où les pointeurs nus seront presque toujours des valeurs empruntées de toute façon.
Elling le
reference_wrapperest supérieur aux pointeurs bruts non seulement parce qu'il est clair qu'il n'est pas propriétaire, mais aussi parce que cela ne peut pas être nullptr(sans manigances) et ainsi les utilisateurs savent qu'ils ne peuvent pas passer nullptr(sans manigances) et vous savez que vous n'êtes pas obligé de le faire vérifiez-le.
underscore_d
19

Vous pouvez le considérer comme un wrapper pratique autour des références afin de pouvoir les utiliser dans des conteneurs.

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

C'est essentiellement une CopyAssignableversion de T&. Chaque fois que vous voulez une référence, mais qu'elle doit être assignable, utilisez std::reference_wrapper<T>ou sa fonction d'assistance std::ref(). Ou utilisez un pointeur.


Autres bizarreries 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

Et comparaison:

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
la source
1
@LaurynasLazauskas On pourrait appeler directement les objets fonction contenus dans le wrapper. Cela est également expliqué dans ma réponse.
Columbo
2
Étant donné que l'implémentation de référence n'est qu'un pointeur à l'intérieur, je ne peux pas comprendre pourquoi les wrappers ajoutent une indirection ou une pénalité de performances
Riga
4
Cela ne devrait pas être plus indirection qu'une simple référence en ce qui concerne un code de version
Riga
3
Je m'attendrais à ce que le compilateur intègre le reference_wrappercode trivial , le rendant identique au code qui utilise un pointeur ou une référence.
David Stone
4
@LaurynasLazauskas: std::reference_wrappera la garantie que l'objet n'est jamais nul. Considérez un membre de la classe std::vector<T *>. Vous devez examiner tout le code de la classe pour voir si cet objet peut jamais stocker un nullptrdans le vecteur, alors qu'avec std::reference_wrapper<T>, vous êtes assuré d'avoir des objets valides.
David Stone