Je me suis penché sur certaines des nouvelles fonctionnalités de C++ 11 et l'une de celles que j'ai remarquées est la double esperluette dans la déclaration de variables, comme T&& var
.
Pour commencer, comment s'appelle cette bête? J'aimerais que Google nous permette de rechercher une ponctuation comme celle-ci.
Qu'est-ce que c'est exactement signifie?
À première vue, cela semble être une double référence (comme les doubles pointeurs de style C, T** var
), mais j'ai du mal à penser à un cas d'utilisation pour cela.
Il déclare un référence de la valeur (doc de la proposition de normes).
Voici une introduction à rvalue références .
Voici un aperçu fantastique des références rvalue par l'une des bibliothèques standard de Microsoft développeurs .
ATTENTION: l'article lié sur MSDN ("Références de valeur: C++ 0x Fonctionnalités dans VC10, partie 2") est une introduction très claire aux références Rvalue, mais fait des déclarations sur les références Rvalue qui étaient autrefois vraies dans le projet de norme C++ 11, mais ne le sont pas pour le dernier! Spécifiquement, il est dit à différents points que les références de valeurs peuvent se lier à des valeurs, ce qui était autrefois vrai, mais a été modifié (par exemple, int x; int && rrx = x; ne compile plus dans GCC) - drewbarbs 13 juil. 14 à 16:12
La plus grande différence entre une référence C++ 03 (maintenant appelée une référence lvalue en C++ 11) est qu’elle peut se lier à une valeur rvalue comme une variable temporaire sans être constante. Ainsi, cette syntaxe est maintenant légale:
T&& r = T();
les références rvalue fournissent principalement les éléments suivants:
Déplacer la sémantique . Il est maintenant possible de définir un constructeur de déplacement et un opérateur d’affectation de déplacement prenant une référence rvalue au lieu de la référence habituelle const-lvalue. Un déplacement fonctionne comme une copie, sauf qu'il n'est pas obligé de garder la source inchangée; en fait, il modifie généralement la source de sorte qu'elle ne possède plus les ressources déplacées. Ceci est idéal pour éliminer les copies superflues, en particulier dans les implémentations de bibliothèques standard.
Par exemple, un constructeur de copie pourrait ressembler à ceci:
foo(foo const& other)
{
this->length = other.length;
this->ptr = new int[other.length];
copy(other.ptr, other.ptr + other.length, this->ptr);
}
Si ce constructeur se voyait attribuer un statut temporaire, la copie serait inutile car nous savons que le temporaire sera simplement détruit; pourquoi ne pas utiliser les ressources du temporaire déjà alloué? En C++ 03, il n'y a aucun moyen d'empêcher la copie car nous ne pouvons pas déterminer si nous avons reçu un temporaire. En C++ 11, nous pouvons surcharger un constructeur de déplacement:
foo(foo&& other)
{
this->length = other.length;
this->ptr = other.ptr;
other.length = 0;
other.ptr = nullptr;
}
Remarquez la grande différence ici: le constructeur de déménagement modifie en fait son argument. Ceci "déplacerait" effectivement le temporaire dans l'objet en construction, éliminant ainsi la copie inutile.
Le constructeur de déplacement serait utilisé pour les références temporaires et non-const qui sont explicitement converties en références rvalue à l'aide de la fonction std::move
(il effectue simplement la conversion). Le code suivant appelle le constructeur de déplacement pour f1
et f2
:
foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"
Transfert parfait . Les références rvalue nous permettent de transférer correctement les arguments des fonctions basées sur des modèles. Prenons par exemple cette fonction d'usine:
template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
return std::unique_ptr<T>(new T(a1));
}
Si nous appelons factory<foo>(5)
, l'argument sera déduit comme étant int&
, qui ne se liera pas à un littéral 5, même si le constructeur de foo
prend un int
. Bien, nous pourrions utiliser à la place A1 const&
, mais que se passe-t-il si foo
prend l'argument du constructeur par référence non-const? Pour créer une fonction d'usine vraiment générique, il faudrait surcharger l'usine sur A1&
et sur A1 const&
. Cela pourrait suffire si l’usine prend 1 type de paramètre, mais chaque type de paramètre supplémentaire multiplierait par 2 la surcharge nécessaire. C’est très rapidement impossible à maintenir.
les références rvalue résolvent ce problème en permettant à la bibliothèque standard de définir une fonction std::forward
pouvant transférer correctement les références lvalue/rvalue. Pour plus d'informations sur le fonctionnement de std::forward
, voir cette excellente réponse .
Cela nous permet de définir la fonction factory comme ceci:
template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}
Maintenant, l'argument rvalue/lvalue-ness est préservé lorsqu'il est passé au constructeur de T
. Cela signifie que si factory est appelé avec une valeur rvalue, le constructeur de T
est appelé avec une valeur rvalue. Si factory est appelé avec une lvalue, le constructeur de T
est appelé avec une lvalue. La fonction d’usine améliorée fonctionne grâce à une règle spéciale:
Lorsque le type de paramètre de fonction est de la forme
T&&
oùT
est un paramètre de modèle et que l’argument de la fonction est une lvalue de typeA
, le typeA&
est utilisé pour déduction d'argument de modèle.
Ainsi, nous pouvons utiliser l’usine comme ceci:
auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1); // calls foo(foo const&)
Propriétés de référence rvalue importantes :
float f = 0f; int&& i = f;
est bien formé parce que float est implicitement convertible en int; la référence serait à un temporaire qui est le résultat de la conversion.std::move
est nécessaire dans: foo&& r = foo(); foo f = std::move(r);
.Il dénote une référence rvalue. Les références Rvalue ne lieront que des objets temporaires, sauf indication explicite. Ils sont utilisés pour rendre les objets beaucoup plus efficaces dans certaines circonstances et pour fournir une installation appelée transfert parfait, ce qui simplifie grandement le code de modèle.
En C++ 03, vous ne pouvez pas faire la distinction entre une copie d'une lvalue non mutable et une rvalue.
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);
En C++ 0x, ce n'est pas le cas.
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);
Considérez l'implémentation derrière ces constructeurs. Dans le premier cas, la chaîne doit effectuer une copie pour conserver la sémantique de valeur, ce qui implique une nouvelle allocation de segment de mémoire. Cependant, dans le second cas, nous savons d'avance que l'objet qui a été transmis à notre constructeur doit immédiatement être détruit et qu'il ne doit pas rester intact. Nous pouvons simplement échanger les pointeurs internes et ne pas effectuer de copie du tout dans ce scénario, qui est considérablement plus efficace. La sémantique de Move profite à toute classe dont la copie coûteuse ou interdite des ressources référencées en interne. Considérons le cas de std::unique_ptr
-. Maintenant que notre classe peut distinguer les temporaires et les non-temporaires, nous pouvons faire en sorte que la sémantique du déplacement fonctionne correctement de sorte que le unique_ptr
ne puisse pas être copié mais puisse être déplacé, ce qui signifie que std::unique_ptr
peut être légalement stocké dans des conteneurs standard, trié, etc., alors que C++ 03 ne peut pas utiliser std::auto_ptr
.
Nous considérons maintenant l’autre utilisation des références rvalue: la transmission parfaite. Considérons la question de lier une référence à une référence.
std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template
Vous ne pouvez pas vous rappeler ce que C++ 03 dit à ce sujet, mais dans C++ 0x, le type résultant lors du traitement de références rvalue est essentiel. Une référence rvalue à un type T, où T est un type de référence, devient une référence de type T.
(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&
Considérons la fonction de modèle la plus simple: min et max En C++ 03, vous devez surcharger manuellement les quatre combinaisons constantes et non constantes. En C++ 0x, c'est juste une surcharge. Combiné avec des modèles variadiques, cela permet une transmission parfaite.
template<typename A, typename B> auto min(A&& aref, B&& bref) {
// for example, if you pass a const std::string& as first argument,
// then A becomes const std::string& and by extension, aref becomes
// const std::string&, completely maintaining it's type information.
if (std::forward<A>(aref) < std::forward<B>(bref))
return std::forward<A>(aref);
else
return std::forward<B>(bref);
}
J'ai omis la déduction de type de retour, car je ne me souviens pas comment cela se fait, mais ce min peut accepter n'importe quelle combinaison de lvalues, rvalues, const lvalues.
Le terme pour T&&
lorsqu'il est utilisé avec une déduction de type (comme pour une transmission parfaite) est connu familièrement sous le nom de référence de transfert . Le terme "référence universelle" a été inventé par Scott Meyers dans cet article , mais a ensuite été modifié.
C'est parce que cela peut être soit la valeur r ou la valeur l.
Les exemples sont:
// template
template<class T> foo(T&& t) { ... }
// auto
auto&& t = ...;
// typedef
typedef ... T;
T&& t = ...;
// decltype
decltype(...)&& t = ...;
Vous trouverez plus de détails dans la réponse à la question suivante: syntaxe pour les références universelles
Une référence rvalue est un type qui se comporte beaucoup comme la référence ordinaire X &, à quelques exceptions près. Le plus important est qu’en ce qui concerne la résolution de surcharge des fonctions, les valeurs préfèrent les références à valeur à l’ancienne, alors que les valeurs préfèrent les nouvelles références:
void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload
X x;
X foobar();
foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
Alors, quelle est une valeur? Tout ce qui n'est pas une valeur. Une lvalue étant une expression qui fait référence à un emplacement de mémoire et nous permet de prendre l'adresse de cet emplacement de mémoire via l'opérateur &.
Il est presque plus facile de comprendre d’abord ce que les valeurs accomplissent avec un exemple:
class Sample {
int *ptr; // large block of memory
int size;
public:
Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz}
{}
// copy constructor that takes lvalue
Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
nullptr}, size{s.size}
{
std::cout << "copy constructor called on lvalue\n";
}
// move constructor that take rvalue
Sample(Sample&& s)
{ // steal s's resources
ptr = s.ptr;
size = s.size;
s.ptr = nullptr; // destructive write
s.size = 0;
cout << "Move constructor called on rvalue." << std::endl;
}
// normal copy assignment operator taking lvalue
Sample& operator=(const Sample& s)
{
if(this != &s) {
delete [] ptr; // free current pointer
ptr = new int[s.size];
size = s.size;
}
cout << "Copy Assignment called on lvalue." << std::endl;
return *this;
}
// overloaded move assignment operator taking rvalue
Sample& operator=(Sample&& lhs)
{
if(this != &s) {
delete [] ptr; //don't let ptr be orphaned
ptr = lhs.ptr; //but now "steal" lhs, don't clone it.
size = lhs.size;
lhs.ptr = nullptr; // lhs's new "stolen" state
lhs.size = 0;
}
cout << "Move Assignment called on rvalue" << std::endl;
return *this;
}
//...snip
};
Le constructeur et les opérateurs d'assignation ont été surchargés avec des versions prenant des références rvalue. Les références de valeur permettent à une fonction de se brancher au moment de la compilation (via la résolution de surcharge) à la condition "Suis-je appelé sur une valeur ou une valeur?". Cela nous a permis de créer des opérateurs de constructeur et d’affectation plus efficaces au-dessus qui déplacent les ressources plutôt que de les copier.
Le compilateur se branche automatiquement au moment de la compilation (selon qu'il est appelé pour une valeur ou une valeur) en choisissant si le constructeur de déplacement ou l'opérateur d'affectation de déplacement doit être appelé.
En résumé: les références rvalue permettent une sémantique de déplacement (et une transmission parfaite, comme indiqué dans le lien de l'article ci-dessous).
Un exemple pratique facile à comprendre est le modèle de classe std :: unique_ptr . Puisqu'un unique_ptr conserve la propriété exclusive de son pointeur brut sous-jacent, il est impossible de copier celui-ci. Cela violerait leur invariant de propriété exclusive. Donc, ils n'ont pas de constructeur de copie. Mais ils ont des constructeurs de déménagement:
template<class T> class unique_ptr {
//...snip
unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};
std::unique_ptr<int[] pt1{new int[10]};
std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.
// So we must first cast ptr1 to an rvalue
std::unique_ptr<int[]> ptr2{std::move(ptr1)};
std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
int size)
{
for (auto i = 0; i < size; ++i) {
param[i] += 10;
}
return param; // implicitly calls unique_ptr(unique_ptr&&)
}
// Now use function
unique_ptr<int[]> ptr{new int[10]};
// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
static_cast<unique_ptr<int[]>&&>(ptr), 10);
cout << "output:\n";
for(auto i = 0; i< 10; ++i) {
cout << new_owner[i] << ", ";
}
output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
static_cast<unique_ptr<int[]>&&>(ptr)
est généralement utilisé avec std :: move
// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);
Un excellent article expliquant tout cela et bien plus encore (avec la manière dont les valeurs permettent une transmission parfaite et ce que cela signifie) avec beaucoup de bons exemples est Thomas Becker C++ Rvalue References Explained . Cet article s'appuyait beaucoup sur son article.
Une introduction plus courte est ne brève introduction aux références à une valeur par Stroutrup, et. Al