web-dev-qa-db-fra.com

Qu'est-ce que "rvalue reference for * this"?

Entré dans une proposition appelée "référence de valeur pour * this" dans Clang's page d'état C++ 11 .

J'ai beaucoup lu sur les références rvalue et les ai bien comprises, mais je ne pense pas être au courant. Je ne pouvais pas non plus trouver 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 du déplacement à * this), mais je ne reçois pas non plus beaucoup d'exemples à partir de là.

De quoi parle cette fonctionnalité?

229
ryaner

Premièrement, "qualificateur de référence pour * ceci" est juste un "énoncé de marketing". Le type de *this _ ne change jamais, voir le bas de cet article. C'est beaucoup plus facile à comprendre avec cette formulation cependant.

Ensuite, le code suivant choisit la fonction à appeler en fonction du qualificateur de référence 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
}

Sortie:

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

Tout est fait pour vous permettre de tirer parti du fait que l'objet sur lequel la fonction est appelée est une valeur rvalue (temporaire non nommé, 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);
  }
};

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

Notez que vous pouvez combiner les qualificatifs cv (const et volatile) et qualificatifs de référence (& et &&).


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

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

La fonction à appeler est déterminée par un processus appelé résolution de surcharge . Ce processus est assez compliqué, nous ne toucherons donc que ce qui est important pour nous.

Premièrement, 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 en utilisant 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, ne 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 contexte peut, le cas échéant, construire une liste d'arguments contenant un argument d'objet implicite pour indiquer l'objet à exploiter.

Pourquoi avons-nous même besoin de comparer les fonctions membres et non membres? La surcharge des opérateurs, voilà pourquoi. Considère ceci:

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

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

Vous voudriez certainement que les personnes suivantes appellent 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 ce que l'on appelle un ensemble de surcharge. Pour rendre la résolution moins compliquée, la partie en gras de la citation standard existe. De plus, ceci 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

  • "Référence de lvalue à cv X" pour les fonctions déclarées sans qualificateur de référence ou avec le & ref-qualifier

  • “Référence rvalue à cv X” pour les fonctions déclarées avec le && ref-qualifier

X est la classe dont la fonction est membre et cv est la qualification cv sur la déclaration de la 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 obéir à 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 celle-ci

[...]

(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 sur lequel une fonction membre (ou un opérateur) est appelée.)

Prenons le premier exemple en haut de ce post. 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 ne contiendra que l'argument d'objet. Voyons à quoi ça 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, une fois toutes les surcharges de l'ensemble testées, il n'en reste qu'une, la résolution de la surcharge réussit 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

Notez cependant que si nous n’avions pas fourni de ref-qualifier (et n’a donc pas surchargé la fonction), que f1 correspondrait à une valeur rvalue (toujours §13.3.1):

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

  • même si le paramètre d'objet implicite n'est pas qualifié const, une valeur rvalue peut être liée au paramètre tant que, dans tous les autres aspects, l'argument peut être converti au type du 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, sur pourquoi la réponse de @ Nicol est au moins partiellement fausse. Il dit:

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

C'est faux, *this est toujours une valeur lvalue:

§5.3.1 [expr.unary.op] p1

Le unaire * l'opérateur effectue indirection : l'expression à laquelle elle est appliquée doit être un pointeur sur un type d'objet ou un pointeur sur un type de fonction et le résultat est une lvalue se rapportant à l'objet ou à la fonction sur laquelle/laquelle l'expression pointe.

§9.3.2 [class.this] p1

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

284
Xeo

Il existe un cas d'utilisation supplémentaire pour le formulaire lvalue ref-qualifier. C++ 98 a un langage qui permet d'appeler des fonctions membres non -const pour les instances de classe rvalues. Cela conduit à toutes sortes d'étranges contradictions avec le concept même d'évaluation et diffère 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 qualificatifs 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 prédéfinis, n'acceptant que les valeurs.

77
JohannesD

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

void SomeFunc() const;
void SomeFunc();

Si une instance de classe n'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 version const. Et le pointeur this est un pointeur const, de sorte que l'instance ne peut pas être modifiée.

La "référence de valeur r" pour this` vous permet d’ajouter une autre alternative:

void RValueFunc() &&;

Cela vous permet d’avoir une fonction qui peut seulement être appelée si l’utilisateur l’appelle via une valeur r appropriée. Donc, s'il s'agit du 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 personnaliser le comportement en fonction de l'accès à l'objet via une valeur r ou non.

Notez que vous n'êtes pas autorisé à surcharger les versions de référence de valeur r et les versions sans référence. C'est-à-dire que si vous avez un nom de fonction membre, toutes ses versions utilisent les qualificateurs l/r-value sur this ou aucun d'entre eux. 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 le && versions tous les membres d'accès 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: les informations suivantes risquent de ne pas correspondre à la version finale de C++ 11; elles découlent directement de la "valeur r initiale 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
28
Nicol Bolas