Що означає T&& (подвійний амперсанд) в C ++ 11?


799

Я розглядав деякі нові функції C ++ 11, і я помітив, що це подвійний амперсанд у оголошенні змінних, як T&& var.

Для початку, як називається цей звір? Я хотів би, щоб Google дозволив нам шукати такі розділові знаки.

Що саме це означає?

На перший погляд, це здається подвійним посиланням (як подвійні покажчики у стилі С T** var), але мені важко продумати випадок використання для цього.


55
Я додав це до c ++ - faq, тому що впевнений, що в майбутньому це з’явиться більше.
GManNickG

3
пов'язане питання про семантику ходу
fredoverflow

41
Ви можете шукати це за допомогою google, вам потрібно лише загортати фразу в лапки: google.com/#q="T%26%26 "тепер ваше запитання є першим зверненням. :)
sbi

Тут є дуже хороша, легка для розуміння відповідь на подібне запитання тут stackoverflow.com/questions/7153991/…
Даніель,

2
У мене вгорі три запитання щодо stackoverflow, які шукають в Google "c ++ два параметра амперсанда", і ваше було першим. Тому для цього вам навіть не потрібно використовувати розділові знаки, якщо ви можете прописати "параметр двох амперсандів".
сергіол

Відповіді:


668

Він оголошує рецензію на значення (стандартна пропозиція doc).

Ось вступ до рецензуючих посилань .

Ось фантастичний глибокий огляд рецензійних посилань одного із стандартних розробників бібліотеки Microsoft .

ПОПЕРЕДЖЕННЯ: пов'язана стаття про MSDN ("Посилання Rvalue: C ++ 0x Особливості у VC10, частина 2") є дуже чітким вступом до посилань на Rvalue, але робить заяви про посилання на Rvalue, які колись були правдивими у проекті C ++ 11 стандартні, але не відповідають дійсності для остаточного! Зокрема, в різних точках сказано, що посилання на оцінку може пов'язуватися з значеннями, що колись було правдою, але було змінено (наприклад, int x; int && rrx = x; більше не компілюється в GCC) - drewbarbs 13 липня 1414 о 16:12

Найбільша різниця між посиланням на C ++ 03 (тепер його називають посиланням на значення в C ++ 11) полягає в тому, що воно може прив'язуватися до рейтингу, як тимчасове, без необхідності const. Таким чином, цей синтаксис тепер законний:

T&& r = T();

Довідники rvalue насамперед передбачають наступне:

Рухайте семантику . Конструктор переміщення та оператор присвоєння переміщення тепер можуть бути визначені, що приймає посилання rvalue замість звичайної посилання const-lvalue. Переміщення функціонує як копія, за винятком того, що воно не зобов'язане зберігати джерело без змін; насправді воно зазвичай модифікує джерело таким чином, що воно більше не володіє переміщеними ресурсами. Це відмінно підходить для усунення сторонніх копій, особливо в стандартних реалізаціях бібліотеки.

Наприклад, конструктор копій може виглядати так:

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

Якби цей конструктор був переданий тимчасовим, копія була б непотрібною, оскільки ми знаємо, що тимчасова буде просто знищена; чому б не скористатися ресурсами тимчасово виділеними? У C ++ 03 немає ніякого способу запобігти копії, оскільки ми не можемо визначити, що ми були прийняті тимчасово. У C ++ 11 ми можемо перевантажити конструктор переміщення:

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

Зауважте тут велику різницю: конструктор ходу фактично змінює свій аргумент. Це ефективно "перемістить" тимчасове в об'єкт, який будується, тим самим усунувши зайву копію.

Конструктор переміщення буде використовуватися для тимчасових посилань та для неконструктивних посилань на значення, які явно перетворюються на посилання rvalue за допомогою std::moveфункції (він просто виконує перетворення). Наступний код викликає конструктор переміщення для f1та 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"

Ідеальне переадресація . посилання rvalue дозволяє нам належним чином пересилати аргументи для шаблонних функцій. Візьмемо для прикладу цю заводську функцію:

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

Якщо ми зателефонували factory<foo>(5), буде виведений аргумент int&, який є , який не буде прив'язуватися до буквального 5, навіть якщо fooконструктор має а int. Добре, ми могли б замість цього використати A1 const&, але що робити, якщо fooаргумент конструктора приймає за допомогою посилань без контрасту? Щоб зробити справді загальну функцію заводу, нам доведеться перевантажувати завод A1&і знову A1 const&. Це може бути добре, якщо фабрика приймає 1 тип параметра, але кожен додатковий тип параметра помножить необхідну перевантаження, встановлену на 2. Це дуже швидко неможливо досягти.

посилання rvalue вирішує цю проблему, дозволяючи стандартній бібліотеці визначити std::forwardфункцію, яка може правильно пересилати посилання lvalue / rvalue. Для отримання додаткової інформації про те, як std::forwardпрацює, дивіться цю відмінну відповідь .

Це дозволяє нам визначити заводську функцію так:

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

Тепер значення rvalue / lvalue-аргументу зберігається при передачі Tконструктору s. Це означає, що якщо завод називається за допомогою rvalue, Tконструктор викликається з rvalue. Якщо завод називається за допомогою значення lvalue, Tконструктор викликається рівнем lvalue. Удосконалена функція заводу працює завдяки одному спеціальному правилу:

Коли тип параметру функції має вигляд, у T&&якому Tпараметр шаблону, а аргумент функції є значенням типу A, тип A&використовується для виведення аргументу шаблону.

Таким чином, ми можемо використовувати фабрику так:

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

Важливі референтні властивості оцінювання :

  • Для роздільної здатності перевантаження lvalues ​​віддають перевагу прив'язці до посилань на lvalue, а rvalues ​​віддають перевагу прив'язці до посилань rvalue . Отже, чому часописи віддають перевагу виклику конструктора переміщення / оператора присвоєння переміщення над конструктором копіювання / оператором призначення.
  • посилання rvalue неявно пов'язуватиметься з rvalues ​​та temporaries, які є результатом неявного перетворення . тобто float f = 0f; int&& i = f;добре сформований, тому що float неявно перетворюється на int; посилання на тимчасове, що є результатом перетворення.
  • Іменовані посилання rvalue - це значення. Неназвані посилання rvalue - це rvalues. Це важливо, щоб зрозуміти, чому std::moveдзвінок необхідний у:foo&& r = foo(); foo f = std::move(r);

65
+1 для Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; не знаючи цього, я намагався зрозуміти, чому люди T &&t; std::move(t);тривалий час роблять двигуни, тощо.
legends2k

@MaximYegorushkin: У цьому прикладі r є обов'язковим до чистого rvalue (тимчасового), і тому тимчасове повинно розширити термін дії, ні?
Пітер Хуене

@PeterHuene Я повертаю це назад, посилання на значення r дійсно продовжує термін служби тимчасового.
Максим Єгорушкін

32
ПОПЕРЕДЖЕННЯ : пов'язана стаття про MSDN ("Посилання Rvalue: C ++ 0x Особливості у VC10, частина 2") є дуже чітким вступом до посилань на Rvalue, але робить заяви про посилання Rvalue, які колись були правдивими у проекті C ++ 11 стандартні, але не відповідають дійсності для остаточного! Зокрема, в різних точках сказано, що посилання на оцінку може пов'язуватися з значеннями, що колись було правдою, але було змінено (наприклад, int x; int &&rrx = x; більше не компілюється в GCC)
drewbarbs

@PeterHuene У наведеному вище прикладі не typename identity<T>::type& aеквівалент T&?
ibp73

81

Він позначає референтне значення. Посилання Rvalue прив'язуватимуть лише до тимчасових об'єктів, якщо явно не породжено інше. Вони використовуються для того, щоб зробити об'єкти набагато ефективнішими за певних обставин та забезпечити об'єкт, відомий як ідеальне переадресація, що значно спрощує код шаблону.

У C ++ 03 ви не можете розрізнити копію не зміненого значення та ревальва.

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

У C ++ 0x це не так.

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

Розглянемо реалізацію цих конструкторів. У першому випадку рядок повинен виконати копію, щоб зберегти семантику значень, що передбачає нове розподілення купи. Однак у другому випадку ми заздалегідь знаємо, що об’єкт, переданий нашому конструктору, негайно підлягає руйнуванню, і він не повинен залишатися недоторканим. Ми можемо ефективно просто поміняти внутрішні покажчики і взагалі не виконувати жодного копіювання в цьому сценарії, що значно ефективніше. Перемістіть семантику на користь будь-якого класу, який має дороге або заборонене копіювання внутрішніх посилань ресурсів. Розглянемо випадок std::unique_ptr- тепер, коли наш клас може розрізняти тимчасові та не-тимчасові, ми можемо змусити семантику переміщення працювати правильно, щоб unique_ptrнеможливо було скопіювати, але можна перемістити, а це означає, щоstd::unique_ptrможна законно зберігати у стандартних контейнерах, сортувати тощо, тоді як C ++ 03's std::auto_ptrне може.

Тепер ми розглянемо інше використання посилань на rvalue - ідеальне переадресація. Розглянемо питання прив’язки посилання на посилання.

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

Не можу пригадати, що C ++ 03 говорить про це, але в C ++ 0x тип результуючого під час роботи з посиланнями rvalue є критичним. Посилання, що посилається на тип T, де T є еталонним типом, стає посиланням типу 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&&

Розглянемо найпростішу функцію шаблону - min та max. У C ++ 03 вам доведеться перевантажувати всі чотири комбінації const та non-const вручну. У C ++ 0x це лише одне перевантаження. У поєднанні з різноманітними шаблонами це дозволяє ідеально пересилати.

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

Я припинив відрахування типу повернення, тому що не можу згадати, як це робиться назовні, але цей хв може приймати будь-яку комбінацію значень, rvalues, const lvalues.


чому ти використовував std::forward<A>(aref) < std::forward<B>(bref)? і я не думаю, що це визначення буде правильним, коли ви спробуєте вперед int&та float&. Краще відкиньте шаблон одного типу форми.
Янкі

25

Термін, T&& коли використовується з відрахуванням типу (наприклад, для ідеального переадресації), розмовно відомий як посилання на переадресацію . Термін "універсальна довідка" був введений Скоттом Мейерсом у цій статті , але згодом був змінений.

Це тому, що це може бути або r-значення, або l-значення.

Приклади:

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

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

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

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

Більше обговорення можна знайти у відповіді на: Синтаксис універсальних посилань


14

Посилання на оцінку - це тип, який веде себе так само, як звичайний посилання X &, за кількома винятками. Найважливішим є те, що коли мова заходить про функцію роздільної здатності перевантаження, lvalues ​​віддає перевагу старим стилям посилання на значення, тоді як rvalues ​​віддає перевагу новим посиланням на rvalue:

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

То що таке ревальва? Все, що не має значення. Значення lvalue - це вираз, який посилається на місце пам'яті і дозволяє нам приймати адресу цього місця пам'яті через & оператор.

Майже простіше зрозуміти спочатку, що оцінюються на прикладі:

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

Оператори конструктора та присвоєння були перевантажені версіями, які беруть посилання на значення. Посилання Rvalue дозволяють функції розгалужуватися під час компіляції (за допомогою роздільної здатності перевантаження) за умови "Чи мене викликають value або rvalue?". Це дозволило нам створити ефективніші оператори конструктора та призначення, які переміщують ресурси, а скопіювати їх.

Компілятор автоматично розгалужується під час компіляції (залежно від того, викликається він для lvalue чи rvalue), вибираючи, чи потрібно викликати конструктор переміщення або оператор присвоєння переміщення.

Підбиття підсумків: посилання rvalue дозволяють пересувати семантику (і ідеальне переадресація, про яку йдеться у посиланні статті нижче).

Один практичний простий для розуміння приклад - шаблон класу std :: unique_ptr . Оскільки унікальний_птр підтримує ексклюзивне право власності на його основний необроблений покажчик, унікальні_птр неможливо скопіювати. Це може порушити їх інваріант виключної власності. Тож у них немає конструкторів копій. Але у них є конструктори переміщення:

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)зазвичай робиться за допомогою std :: move

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

Відмінна стаття, що пояснює все це та інше (наприклад, як оцінки дозволяють ідеальне переадресація та що це означає) з великою кількістю хороших прикладів, - пояснені посилання на C ++ Rvalue Томаса Бекера . Цей пост значною мірою спирався на свою статтю.

Коротший вступ - Короткий вступ до рецензійних посилань від Stroutrup, et. ін


Чи не так, що конструктору копій Sample(const Sample& s)також потрібно скопіювати вміст? Те саме питання щодо "оператора присвоєння копії".
К.Карамазен

Так, ти правий. Не вдалося скопіювати пам'ять. Конструктор копій та оператор призначення копії повинні робити memcpy (ptr, s.ptr, size) після тестування цього розміру! = 0. І конструктор за замовчуванням повинен робити memset (ptr, 0, size), якщо розмір! = 0.
kurt krueckeberg

Добре, дякую. Таким чином, цей коментар та попередні два коментарі можуть бути усунені, оскільки проблема також була виправлена ​​у відповіді.
К.Карамазен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.