Qu'est-ce que la «référence de valeur pour * this»?

238

Je suis tombé sur une proposition appelée "référence rvalue pour * this" dans la page d'état C ++ 11 de clang .

J'ai beaucoup lu sur les références rvalue et les ai comprises, mais je ne pense pas que je sache cela. Je n'ai pas non plus trouvé beaucoup de ressources sur le Web en utilisant les termes.

Il y a un lien vers le document de proposition sur la page: N2439 (Extension de la sémantique de déplacement à * this), mais je ne reçois pas non plus beaucoup d'exemples à partir de là.

De quoi s'agit-il?

ryaner
la source

Réponses:

293

Tout d'abord, les «qualificatifs de référence pour * ceci» ne sont qu'une «déclaration marketing». Le type de *thisne change jamais, voir le bas de cet article. Il est cependant beaucoup plus facile de le comprendre avec cette formulation.

Ensuite, le code suivant choisit la fonction à appeler en fonction du qualificatif ref du "paramètre d'objet implicite" de la fonction :

// t.cpp
#include <iostream>

struct test{
  void f() &{ std::cout << "lvalue object\n"; }
  void f() &&{ std::cout << "rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // lvalue
  test().f(); // rvalue
}

Production:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

Le tout est fait pour vous permettre de profiter du fait que l'objet auquel la fonction est appelée est une rvalue (temporaire sans nom, par exemple). Prenez le code suivant comme exemple supplémentaire:

struct test2{
  std::unique_ptr<int[]> heavy_resource;

  test2()
    : heavy_resource(new int[500]) {}

  operator std::unique_ptr<int[]>() const&{
    // lvalue object, deep copy
    std::unique_ptr<int[]> p(new int[500]);
    for(int i=0; i < 500; ++i)
      p[i] = heavy_resource[i];

    return p;
  }

  operator std::unique_ptr<int[]>() &&{
    // rvalue object
    // we are garbage anyways, just move resource
    return std::move(heavy_resource);
  }
};

Cela peut être un peu artificiel, mais vous devriez avoir l'idée.

Notez que vous pouvez combiner les qualificatifs cv ( constet volatile) et les qualificatifs ref ( &et &&).


Remarque: De nombreuses citations standard et une explication de la résolution de surcharge après ici!

† Pour comprendre comment cela fonctionne et pourquoi la réponse de @Nicol Bolas est au moins partiellement fausse, nous devons creuser un peu la norme C ++ (la partie expliquant pourquoi la réponse de @ Nicol est fausse se trouve en bas, si vous êtes seulement intéressé par cela).

La fonction qui sera appelée est déterminée par un processus appelé résolution de surcharge . Ce processus est assez compliqué, donc nous ne toucherons que la partie qui est importante pour nous.

Tout d'abord, il est important de voir comment fonctionne la résolution de surcharge pour les fonctions membres:

§13.3.1 [over.match.funcs]

p2 L'ensemble des fonctions candidates peut contenir des fonctions membres et non membres à résoudre par rapport à la même liste d'arguments. Pour que les listes d'arguments et de paramètres soient comparables dans cet ensemble hétérogène, une fonction membre est considérée comme ayant un paramètre supplémentaire, appelé paramètre d'objet implicite, qui représente l'objet pour lequel la fonction membre a été appelée . [...]

p3 De même, le cas échéant, le contexte peut construire une liste d'arguments qui contient un argument d'objet implicite pour désigner l'objet à opérer.

Pourquoi devons-nous même comparer les fonctions membres et non membres? Surcharge de l'opérateur, c'est pourquoi. Considère ceci:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

Vous voudriez certainement que ce qui suit appelle la fonction gratuite, n'est-ce pas?

char const* s = "free foo!\n";
foo f;
f << s;

C'est pourquoi les fonctions membres et non membres sont incluses dans le soi-disant jeu de surcharge. Pour rendre la résolution moins compliquée, la partie en gras de la citation standard existe. De plus, c'est le bit important pour nous (même clause):

p4 Pour les fonctions membres non statiques, le type du paramètre d'objet implicite est

  • «Lvalue reference to cv X » pour les fonctions déclarées sans qualificatif ref ou avec le & qualificatif ref

  • «Rvalue référence to cv X » pour les fonctions déclarées avec le && qualificatif ref

Xest la classe dont la fonction est membre et cv est la qualification cv sur la déclaration de fonction membre. [...]

p5 Pendant la résolution de surcharge, [...] le paramètre d'objet implicite [...] conserve son identité puisque les conversions sur l'argument correspondant doivent respecter ces règles supplémentaires:

  • aucun objet temporaire ne peut être introduit pour contenir l'argument du paramètre d'objet implicite; et

  • aucune conversion définie par l'utilisateur ne peut être appliquée pour obtenir une correspondance de type avec elle

[...]

(Le dernier bit signifie simplement que vous ne pouvez pas tricher avec une résolution de surcharge basée sur des conversions implicites de l'objet auquel une fonction membre (ou opérateur) est appelée.)

Prenons le premier exemple en haut de cet article. Après la transformation susmentionnée, l'ensemble de surcharge ressemble à ceci:

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

Ensuite, la liste d'arguments, contenant un argument d'objet implicite , est comparée à la liste de paramètres de chaque fonction contenue dans l'ensemble de surcharge. Dans notre cas, la liste d'arguments contiendra uniquement cet argument d'objet. Voyons à quoi cela ressemble:

// first call to 'f' in 'main'
test t;
f1(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

Si, après que toutes les surcharges de l'ensemble ont été testées, il n'en reste qu'une, la résolution de surcharge a réussi et la fonction liée à cette surcharge transformée est appelée. Il en va de même pour le deuxième appel à «f»:

// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
            // taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
            // kept in overload-set

A noter toutefois que, si nous ne fourni aucune ref-qualification (et en tant que telle ne surcharge la fonction), qui f1 serait correspondre à une rvalue (encore §13.3.1):

p5 [...] Pour les fonctions membres non statiques déclarées sans qualificatif ref , une règle supplémentaire s'applique:

  • même si le paramètre d'objet implicite n'est pas constqualifié, une valeur r peut être liée au paramètre tant que, à tous autres égards, l'argument peut être converti en type de paramètre d'objet implicite.
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

Maintenant, pourquoi la réponse de @ Nicol est au moins en partie fausse. Il dit:

Notez que cette déclaration change le type de *this.

C'est faux, *thisc'est toujours une valeur l:

§5.3.1 [expr.unary.op] p1

L' *opérateur unaire effectue une indirection : l'expression à laquelle il est appliqué doit être un pointeur vers un type d'objet, ou un pointeur vers un type de fonction et le résultat est une valeur l se référant à l'objet ou à la fonction vers laquelle l'expression pointe.

§9.3.2 [class.this] p1

Dans le corps d'une fonction membre non statique (9.3), le mot this- clé est une expression de valeur dont la valeur est l'adresse de l'objet pour lequel la fonction est appelée. Le type de thisdans une fonction membre d'une classe Xest X*. [...]

Xeo
la source
Je crois que les types de paraneter juste après la section "après la transformation" devraient être "foo" au lieu de "test".
ryaner
@ryaner: Bonne trouvaille, merci. Bien que ce ne soit pas le paramètre, mais l'identificateur de classe des fonctions est faux. :)
Xeo
oups désolé j'ai oublié la classe de jouets nommée test quand j'ai lu cette partie et j'ai pensé que f est contenu dans foo donc mon commentaire ..
ryaner
Cela peut-il être fait avec des constructeurs MyType(int a, double b) &&:?
Germán Diago
2
"Le type de * cela ne change jamais" Vous devriez peut-être être un peu plus clair qu'il ne change pas en fonction de la qualification de la valeur r / l. mais il peut changer entre const / non-const.
xaxxon
78

Il existe un cas d'utilisation supplémentaire pour le formulaire lvalue ref-qualifier. C ++ 98 possède un langage qui permet constd'appeler des fonctions non membres pour des instances de classe qui sont des valeurs r. Cela conduit à toutes sortes de bizarreries qui vont à l'encontre du concept même d'évaluation et s'écartent du fonctionnement des types intégrés:

struct S {
  S& operator ++(); 
  S* operator &(); 
};
S() = S();      // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S();           // taking address of rvalue...

Les qualificateurs de référence Lvalue résolvent ces problèmes:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

Maintenant, les opérateurs fonctionnent comme ceux des types intégrés, acceptant uniquement les valeurs l.

JohannesD
la source
28

Disons que vous avez deux fonctions sur une classe, toutes deux portant le même nom et la même signature. Mais l'un d'eux est déclaré const:

void SomeFunc() const;
void SomeFunc();

Si une instance de classe ne l'est pas const, la résolution de surcharge sélectionnera préférentiellement la version non const. Si l'instance est const, l'utilisateur ne peut appeler que la constversion. Et le thispointeur est un constpointeur, donc l'instance ne peut pas être modifiée.

La "référence de valeur r pour cela" vous permet d'ajouter une autre alternative:

void RValueFunc() &&;

Cela vous permet d'avoir une fonction qui ne peut être appelée que si l'utilisateur l'appelle via une valeur r appropriée. Donc, si c'est dans le type Object:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

De cette façon, vous pouvez spécialiser le comportement en fonction de l'accès à l'objet via une valeur r ou non.

Notez que vous n'êtes pas autorisé à surcharger entre les versions de référence de valeur r et les versions non de référence. Autrement dit, si vous avez un nom de fonction membre, toutes ses versions utilisent les qualificatifs l / r-value sur this, ou aucune ne le fait. Vous ne pouvez pas faire ça:

void SomeFunc();
void SomeFunc() &&;

Vous devez faire ceci:

void SomeFunc() &;
void SomeFunc() &&;

Notez que cette déclaration change le type de *this. Cela signifie que les &&versions accèdent à tous les membres en tant que références de valeur r. Il devient donc possible de se déplacer facilement de l'intérieur de l'objet. L'exemple donné dans la première version de la proposition est (remarque: ce qui suit peut ne pas être correct avec la version finale de C ++ 11; il provient directement de la "valeur r de cette proposition"):

class X {
   std::vector<char> data_;
public:
   // ...
   std::vector<char> const & data() const & { return data_; }
   std::vector<char> && data() && { return data_; }
};

X f();

// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move
Nicol Bolas
la source
2
Je pense que vous avez besoin de std::movela deuxième version, non? Aussi, pourquoi la référence rvalue renvoie-t-elle?
Xeo
1
@Xeo: Parce que c'est l'exemple de la proposition; Je n'ai aucune idée si cela fonctionne toujours avec la version actuelle. Et la raison du retour de référence de la valeur r est que le mouvement doit être à la personne qui le capture. Cela ne devrait pas encore arriver, juste au cas où il voudrait le stocker dans un && au lieu d'une valeur.
Nicol Bolas
3
Bon, j'ai un peu pensé à la raison de ma deuxième question. Je me demande cependant si une référence de valeur au membre d'un temporaire prolonge la durée de vie de ce temporaire ou de son membre? Je pourrais jurer avoir vu une question à ce sujet sur SO il y a quelque temps ...
Xeo
1
@Xeo: Ce n'est pas entièrement vrai. La résolution de surcharge choisira toujours la version non constante si elle existe. Vous auriez besoin de faire un cast pour obtenir la version const. J'ai mis à jour le message pour clarifier.
Nicol Bolas
15
J'ai pensé que je pourrais expliquer, après tout, j'ai créé cette fonctionnalité pour C ++ 11;) Xeo a raison d'insister sur le fait qu'il ne change pas le type de *this, cependant je peux comprendre d'où vient la confusion. Cela est dû au fait que le qualificatif ref modifie le type de paramètre de fonction implicite (ou "caché") auquel "cet objet" (guillemets mis ici exprès!) Est lié pendant la résolution de surcharge et l'appel de fonction. Donc, pas de changement *thispuisque c'est corrigé comme l'explique Xeo. Au lieu de cela, changez le paramètre "hiddden" pour le rendre lvalue- ou rvalue-reference, tout comme le constqualificatif de fonction le fait constetc.
bronekk