Co oznacza T&& (double ampersand) w C ++ 11?


799

Przyglądałem się niektórym nowym funkcjom C ++ 11 i zauważyłem, że podwójny znak & w deklaracji zmiennych, takich jak T&& var.

Na początek, jak nazywa się ta bestia? Chciałbym, aby Google umożliwił nam wyszukiwanie takich znaków interpunkcyjnych.

Co to dokładnie znaczy?

Na pierwszy rzut oka wydaje się, że jest to podwójne odniesienie (podobnie jak podwójne wskaźniki w stylu C T** var), ale trudno mi się zastanowić nad przypadkiem użycia w tym celu.


55
Dodałem to do c ++ - faq, ponieważ jestem pewien, że pojawi się więcej w przyszłości.
GManNickG 30.03.11

3
powiązane pytanie dotyczące semantyki ruchów
fredoverflow

41
Możesz wyszukiwać za pomocą Google, musisz tylko zawrzeć swoje zdanie w cudzysłowie: google.com/#q="T%26%26 ”ma teraz Twoje pytanie jako pierwsze trafienie. :)
sbi

Istnieje bardzo dobra, łatwa do zrozumienia odpowiedź na podobne pytanie tutaj stackoverflow.com/questions/7153991/…
Daniel

2
Na górze mam trzy pytania dotyczące przepełnienia stosu, szukając w Google „parametru c ++ dwa znaki ampersands”, a twoje było pierwsze. Nie musisz nawet używać do tego interpunkcji, jeśli możesz przeliterować „parametr dwóch ampersand”.
sergiol

Odpowiedzi:


668

Deklaruje odniesienie do wartości (dokument propozycji norm).

Oto wstępem do rvalue odniesień .

Oto fantastyczny wygląd pogłębione w rvalue odnośnikach jednej standardowej biblioteki Microsoft programistów .

UWAGA: powiązany artykuł na temat MSDN („Odwołania do wartości: C ++ 0x Funkcje w VC10, część 2”) jest bardzo jasnym wprowadzeniem do odniesień do Rvalue, ale zawiera stwierdzenia o odniesieniach do wartości, które kiedyś były prawdziwe w wersji roboczej C ++ 11 standardowe, ale nie są prawdziwe w przypadku ostatniego! Mówi w szczególności, że w różnych punktach odniesienia do wartości mogą wiązać się z wartościami, które kiedyś były prawdziwe, ale zostały zmienione (np. Int x; int &&rrx = x; już nie kompiluje się w GCC) - drawbarbs 13 lipca 2014 o 16:12

Największą różnicą między referencją C ++ 03 (obecnie nazywaną referencją lvalue w C ++ 11) jest to, że może ona wiązać się z wartością jak tymczasowa bez konieczności bycia const. Zatem ta składnia jest teraz legalna:

T&& r = T();

odniesienia do wartości uwzględniają przede wszystkim:

Przenieś semantykę . Można teraz zdefiniować konstruktor ruchu i operator przypisania ruchu, który pobiera odwołanie do wartości zamiast zwykłego odwołania do wartości stałej. Ruch działa jak kopia, ale nie jest zobowiązany do zachowania źródła bez zmian; w rzeczywistości zwykle modyfikuje źródło tak, że nie jest już właścicielem przeniesionych zasobów. Jest to idealne rozwiązanie do eliminowania obcych kopii, szczególnie w standardowych implementacjach bibliotek.

Na przykład konstruktor kopii może wyglądać następująco:

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}

Jeśli ten konstruktor przejdzie tymczasowo, kopia byłaby niepotrzebna, ponieważ wiemy, że tymczasowy zostanie po prostu zniszczony; dlaczego nie skorzystać z zasobów tymczasowo już przydzielonych? W C ++ 03 nie ma sposobu, aby zapobiec kopiowaniu, ponieważ nie jesteśmy w stanie stwierdzić, że otrzymaliśmy tymczasowe zezwolenie. W C ++ 11 możemy przeciążać konstruktor ruchu:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

Zauważ tutaj dużą różnicę: konstruktor ruchu faktycznie modyfikuje swój argument. To skutecznie „przeniósłby” tymczasowość do budowanego obiektu, eliminując w ten sposób niepotrzebną kopię.

Konstruktor ruchu byłby używany do tymczasowych i nie stałych const referencji wartości, które są jawnie konwertowane na referencje wartości za pomocą std::movefunkcji (po prostu wykonuje konwersję). Poniższy kod wywołuje konstruktor przenoszenia dla f1i 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"

Idealne przekazywanie . Odwołania do wartości pozwalają nam poprawnie przekazywać argumenty dla funkcji szablonowych. Weźmy na przykład tę funkcję fabryczną:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}

Jeśli zadzwonimy factory<foo>(5), wywnioskowany zostanie argument int&, który nie będzie wiązał się z literałem 5, nawet jeśli fookonstruktor przyjmuje argument int. Cóż, moglibyśmy zamiast tego użyć A1 const&, ale co jeśli fooargument konstruktora zostanie przyjęty przez odwołanie inne niż const? Aby prawdziwie rodzajowe funkcji fabryki, mielibyśmy do przeciążenia fabrykę na A1&i na A1 const&. Może to być w porządku, jeśli fabryka przyjmuje 1 typ parametru, ale każdy dodatkowy typ parametru pomnożyłby niezbędne ustawienie przeciążenia przez 2. To bardzo szybko nie do utrzymania.

odwołania do wartości rozwiązują ten problem, umożliwiając standardowej bibliotece zdefiniowanie std::forwardfunkcji, która może poprawnie przekazywać odniesienia do wartości / wartości. Aby uzyskać więcej informacji o tym std::forward, jak działa, zobacz tę doskonałą odpowiedź .

To pozwala nam zdefiniować funkcję fabryczną w następujący sposób:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
    return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}

Teraz argument rvalue / lvalue-ness argumentu zostaje zachowany po przekazaniu do Tkonstruktora. Oznacza to, że jeśli wywoływana jest fabryka z wartością, Tkonstruktor jest wywoływany z wartością. Jeśli fabryka jest wywoływana z wartością, Tkonstruktor jest wywoływany z wartością. Ulepszona funkcja fabryczna działa dzięki jednej specjalnej zasadzie:

Gdy typ parametru funkcji ma postać, w T&&której Tjest parametrem szablonu, a argument funkcji jest wartością typu A, typ A&służy do odejmowania argumentu szablonu.

W ten sposób możemy użyć fabryki tak:

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

Ważne właściwości odniesienia wartości :

  • W celu rozwiązania problemu z przeciążeniem wartości lv wolą wiązanie od odwołań do wartości, zaś wartości wolą wiązanie od odwołań do wartości . Dlatego tymczasowe wolą wywoływać konstruktor ruchu / operator przypisania ruchu niż konstruktor kopiowania / operator przypisania.
  • Odwołania do wartości będą domyślnie wiązać się z wartościami i tymczasowymi, które są wynikiem niejawnej konwersji . tzn. float f = 0f; int&& i = f;jest dobrze uformowany, ponieważ liczba zmiennoprzecinkowa jest domyślnie przekształcalna na int; odniesienie dotyczyłoby tymczasowego wyniku konwersji.
  • Nazwane odniesienia wartości są wartościami lv. Nienazwane odniesienia do wartości są wartościami. Ważne jest, aby zrozumieć, dlaczego std::movepołączenie jest konieczne w:foo&& r = foo(); foo f = std::move(r);

65
+1 dla Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; nie wiedząc o tym, starałem się zrozumieć, dlaczego ludzie T &&t; std::move(t);od dawna robią sobie lekarzy przeprowadzek i tym podobne.
legends2k

@MaximYegorushkin: W tym przykładzie r wiąże się z czystą wartością (tymczasową), a zatem zakres tymczasowy powinien zostać rozszerzony na całe życie, nie?
Peter Huene

@PeterHuene Cofam to, odwołanie do wartości r wydłuża czas życia tymczasowego.
Maxim Egorushkin

32
UWAGA : powiązany artykuł na temat MSDN („Odwołania do wartości: C ++ 0x Funkcje w VC10, część 2”) jest bardzo jasnym wprowadzeniem do odniesień do Rvalue, ale zawiera stwierdzenia o odniesieniach do wartości, które kiedyś były prawdziwe w wersji roboczej C ++ 11 standardowe, ale nieprawdziwe w przypadku ostatniego! Mówi w szczególności, że w różnych punktach odniesienia do wartości mogą wiązać się z wartościami, które kiedyś były prawdziwe, ale zostały zmienione (np. int x; int &&rrx = x; Nie kompilują się już w GCC)
drawbarbs

@PeterHuene W powyższym przykładzie nie jest typename identity<T>::type& arównoważne z T&?
ibp73,

81

Oznacza odniesienie do wartości. Odwołania do wartości będą się wiązały tylko z obiektami tymczasowymi, chyba że zostanie wyraźnie wygenerowane inaczej. Służą do zwiększania wydajności obiektów w określonych okolicznościach oraz do zapewnienia funkcji znanej jako doskonałe przekazywanie, co znacznie upraszcza kod szablonu.

W C ++ 03 nie można odróżnić kopii niemodyfikowalnej wartości od wartości.

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&);

W C ++ 0x tak nie jest.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);

Zastanów się nad implementacją tych konstruktorów. W pierwszym przypadku ciąg musi wykonać kopię, aby zachować semantykę wartości, co obejmuje nowy przydział sterty. Jednak w drugim przypadku wiemy z góry, że obiekt, który został przekazany naszemu konstruktorowi, jest natychmiast spowodowany zniszczeniem i nie musi pozostać nietknięty. W tym scenariuszu, który jest znacznie bardziej wydajny, możemy skutecznie zamienić wewnętrzne wskaźniki i nie wykonywać żadnego kopiowania. Przenieś semantykę na korzyść każdej klasy, która ma kosztowne lub zabronione kopiowanie wewnętrznych zasobów. Rozważmy przypadek std::unique_ptr- teraz, gdy nasza klasa potrafi rozróżniać między tymczasowymi i nie-tymczasowymi, możemy sprawić, aby semantyka przenoszenia działała poprawnie, tak aby unique_ptrnie można było skopiować, ale można ją przenieść, co oznacza, żestd::unique_ptrmogą być legalnie przechowywane w standardowych kontenerach, sortowane itp., podczas gdy C ++ 03 std::auto_ptrnie.

Teraz rozważamy inne zastosowanie referencji wartości - idealne przekazywanie. Rozważ kwestię związania odniesienia do odniesienia.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

Nie mogę sobie przypomnieć, co na ten temat mówi C ++ 03, ale w C ++ 0x typ wynikowy podczas obsługi odwołań do wartości jest krytyczny. Odwołanie do wartości typu T, gdzie T jest typem odniesienia, staje się odwołaniem typu 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&&

Rozważ najprostszą funkcję szablonu - min. I maks. W C ++ 03 musisz ręcznie przeciążać wszystkie cztery kombinacje const i non-const. W C ++ 0x to tylko jedno przeciążenie. W połączeniu z różnymi szablonami umożliwia to idealne przekazywanie.

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);
}

Zrezygnowałem z odliczenia typu zwrotu, ponieważ nie mogę sobie przypomnieć, jak to się robi odręcznie, ale ta min może zaakceptować dowolną kombinację wartości lvalu, wartości rv, wartości stałych lv.


dlaczego użyłeś std::forward<A>(aref) < std::forward<B>(bref)? i nie sądzę, aby ta definicja była poprawna, gdy spróbujesz naprzód int&i float&. Lepiej upuść szablon formularza jednego typu.
Yankes

25

Termin T&& używany w przypadku dedukcji typu (na przykład do idealnego przekazywania) jest znany potocznie jako odniesienie do przekazywania . Termin „uniwersalne odniesienie” został wymyślony przez Scotta Meyersa w tym artykule , ale później został zmieniony.

Jest tak, ponieważ może to być wartość r lub wartość l.

Przykładami są:

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

Więcej dyskusji można znaleźć w odpowiedzi na: Składnia uniwersalnych odniesień


14

Odwołanie do wartości jest typem, który zachowuje się podobnie jak zwykłe odwołanie X i, z kilkoma wyjątkami. Najważniejsze jest to, że jeśli chodzi o rozdzielczość przeciążenia funkcji, wartości lv preferują stare wartości referencyjne wartości, podczas gdy wartości wolą nowe referencje wartości:

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&&)

Czym jest wartość? Wszystko, co nie jest wartością. Wartość jest wyrażeniem odnoszącym się do lokalizacji w pamięci i pozwala nam pobrać adres tej lokalizacji za pośrednictwem operatora &.

Prawie łatwiej jest najpierw zrozumieć, co wartości osiągają na przykładzie:

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, 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
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     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
};     

Konstruktor i operatory przypisania zostały przeciążone wersjami, które pobierają odwołania do wartości. Odwołania do wartości pozwalają na rozgałęzienie funkcji w czasie kompilacji (przez rozwiązanie przeciążenia) pod warunkiem „Czy jestem wywoływany na wartości lub wartości?”. To pozwoliło nam stworzyć bardziej wydajne konstruktory i operatory przypisania powyżej, które przenoszą zasoby raczej je kopiują.

Kompilator automatycznie rozgałęzia się w czasie kompilacji (w zależności od tego, czy jest wywoływany dla wartości czy wartości), wybierając, czy należy wywołać konstruktora ruchu, czy operator przypisania ruchu.

Podsumowując: odniesienia do wartości pozwalają na semantykę przenoszenia (i doskonałe przekazywanie, omówione w linku do artykułu poniżej).

Jednym praktycznym, łatwym do zrozumienia przykładem jest szablon klasy std :: unique_ptr . Ponieważ Unique_ptr utrzymuje wyłączną własność swojego podstawowego wskaźnika surowego, unikalne pliki_ptr nie mogą być kopiowane. Naruszyłoby to ich niezmienność wyłącznej własności. Nie mają więc konstruktorów kopiowania. Ale mają konstruktory ruchów:

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)zwykle odbywa się za pomocą std :: move

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

Doskonałym artykułem wyjaśniającym to wszystko i wiele więcej (na przykład, w jaki sposób wartości pozwalają na idealne przekazywanie i co to oznacza) z wieloma dobrymi przykładami jest wyjaśnienie referencji wartości C ++ Thomasa Beckera . Ten post opierał się w dużej mierze na jego artykule.

Krótsze wprowadzenie to Krótkie wprowadzenie do odniesień do wartości przez Stroutrup, i in. glin


Czyż nie jest tak, że konstruktor kopiujący Sample(const Sample& s)musi również skopiować zawartość? To samo pytanie dla „operatora przypisania kopii”.
K.Karamazen

Tak masz rację. Nie udało się skopiować pamięci. Konstruktor kopiujący i operator przypisania kopii powinni wykonać memcpy (ptr, s.ptr, size) po przetestowaniu tego rozmiaru! = 0. A domyślny konstruktor powinien wykonać memset (ptr, 0, rozmiar), jeśli rozmiar! = 0.
kurt krueckeberg

Ok, dzięki. W ten sposób ten komentarz i dwa poprzednie komentarze można usunąć, ponieważ problem został również rozwiązany w odpowiedzi.
K.Karamazen
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.