Что означает T && (двойной амперсанд) в C ++ 11?

801

Я изучил некоторые из новых функций C ++ 11, и одна из них я заметил, это двойной амперсанд при объявлении переменных, например T&& var.

Для начала, как зовут этого зверя? Я бы хотел, чтобы Google позволил нам искать такие знаки препинания, как этот.

Что именно это значит?

На первый взгляд, это, кажется, двойная ссылка (как двойные указатели в стиле C T** var), но мне трудно придумать вариант использования для этого.

paxdiablo
источник
55
Я добавил это в c ++ - faq, так как я уверен, что это произойдет в будущем.
GManNickG
3
связанный вопрос о семантике перемещения
fredoverflow
41
Вы можете искать это с помощью Google, вам нужно только заключить вашу фразу в кавычки: google.com/#q="T%26%26 "теперь ваш вопрос является первым хитом. :)
СБИ
Здесь есть очень хороший, легкий для понимания ответ на подобный вопрос stackoverflow.com/questions/7153991/…
Даниэль
2
Я получил три вопроса от stackoverflow в верхнем поиске в Google по «параметру c ++ two ampersands», и ваш был первым. Так что вам даже не нужно использовать пунктуацию для этого, если вы можете прописать «параметр двух амперсандов».
sergiol

Ответы:

668

Он объявляет ссылку на rvalue (стандартное предложение doc).

Вот введение в rvalue ссылки .

Вот фантастический взгляд в глубину на RValue ссылок по одной из стандартных библиотек Microsoft, разработчиков .

ВНИМАНИЕ: связанная статья по MSDN («Ссылки на Rvalue: функции C ++ 0x в VC10, часть 2») является очень четким введением в ссылки на Rvalue, но содержит утверждения о ссылках на Rvalue, которые когда-то были верны в проекте C ++ 11 стандартная, но не верная для финальной! В частности, в различных точках говорится, что ссылки на rvalue могут связываться с lvalue, что когда-то было истинно, но было изменено (например, int x; int && rrx = x; больше не компилируется в GCC) - drewbarbs 13 июля '14 в 16:12

Самым большим отличием ссылки C ++ 03 (теперь называемой ссылкой lvalue в C ++ 11) является то, что она может связываться с rvalue как временная, не будучи константой. Таким образом, этот синтаксис теперь допустим:

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

Обратите внимание на большую разницу: конструктор перемещения фактически изменяет свой аргумент. Это эффективно «переместит» временный объект в создаваемый объект, тем самым устраняя ненужную копию.

Конструктор перемещения будет использоваться для временных значений и для неконстантных lvalue-ссылок, которые явно преобразуются в 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-ness сохраняется при передаче в Tконструктор. Это означает, что если фабрика вызывается с помощью rvalue, то Tконструктор вызывается с помощью rvalue. Если factory вызывается с lvalue, то Tконструктор вызывается с lvalue. Улучшенная фабричная функция работает благодаря одному специальному правилу:

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

Таким образом, мы можем использовать фабрику так:

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

Важные справочные свойства :

  • Для разрешения перегрузки lvalue предпочитает привязку к ссылкам lvalue, а rvalues ​​предпочитает привязку к ссылкам rvalue . Следовательно, временные производители предпочитают вызывать конструктор перемещения / оператор присваивания перемещения по сравнению с конструктором копирования / оператором присваивания.
  • Ссылки на rvalue будут неявно связываться с rvalue и временными значениями, которые являются результатом неявного преобразования . т.е. float f = 0f; int&& i = f;хорошо сформирован, потому что float неявно конвертируется в int; ссылка была бы на временную, которая является результатом преобразования.
  • Именованные ссылки на rvalue являются lvalues. Безымянный rvalue ссылки являются rvalues. Это важно, чтобы понять, почему std::moveвызов необходим в:foo&& r = foo(); foo f = std::move(r);
Peter Huene
источник
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 стандартный, но не верный для финального! В частности, в различных точках говорится, что ссылки на rvalue могут связываться с lvalue, что когда-то было истиной, но было изменено (например, int x; int &&rrx = x; больше не компилируется в GCC)
drewbarbs
@PeterHuene В приведенном выше примере не typename identity<T>::type& aэквивалентно T&?
ibp73
81

Это обозначает rvalue ссылку. Ссылки Rvalue будут привязываться только к временным объектам, если явно не сгенерировано иначе. Они используются для того, чтобы сделать объекты намного более эффективными при определенных обстоятельствах, а также для предоставления средства, известного как совершенная пересылка, что значительно упрощает код шаблона.

В C ++ 03 вы не можете различить копию неизменяемого lvalue и 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&);

В 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 не std::auto_ptrможет.

Теперь мы рассмотрим другое использование ссылок на rvalue - идеальная пересылка. Рассмотрим вопрос привязки ссылки к ссылке.

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

Не могу вспомнить, что C ++ 03 говорит по этому поводу, но в C ++ 0x результирующий тип при работе со ссылками на rvalue является критическим. Ссылка 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);
}

Я прекратил вычитание возвращаемого типа, потому что не могу вспомнить, как это делается не по назначению, но этот min может принимать любую комбинацию lvalues, 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 = ...;

Дополнительную информацию можно найти в ответе на: Синтаксис для универсальных ссылок

mmocny
источник
14

Ссылка rvalue - это тип, который ведет себя так же, как обычная ссылка X &, с несколькими исключениями. Наиболее важным является то, что когда речь идет о разрешении перегрузки функций, lvalue предпочитает ссылки lvalue старого стиля, тогда как 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. 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 ссылки. Ссылки на Rvalue позволяют функции разветвляться во время компиляции (с помощью разрешения перегрузки) при условии «Меня вызывают по lvalue или по rvalue?». Это позволило нам создать более эффективные операторы конструктора и присваивания выше, которые перемещают ресурсы, а не копируют их.

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

Подводя итог: ссылки на rvalue допускают семантику перемещения (и идеальную пересылку, обсуждаемую в статье ниже).

Одним из практических простых для понимания примеров является шаблон класса std :: unique_ptr . Так как unique_ptr поддерживает исключительное владение своим базовым необработанным указателем, 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);

Отличная статья, объясняющая все это и многое другое (например, как значения rvalue позволяют совершенную пересылку и что это значит) с множеством хороших примеров, - объяснение ссылок на C ++ Rvalue Томаса Беккера . Этот пост в значительной степени опирался на его статью.

Более короткое введение - Краткое введение в Rvalue References от Stroutrup, et. аль

Курт Крюкеберг
источник
Разве это не так, что конструктор копирования Sample(const Sample& s)должен также копировать содержимое? Тот же вопрос для «оператора присваивания копии».
К.Карамазен
Да ты прав. Мне не удалось скопировать память. Конструктор копирования и оператор копирования должны оба делать memcpy (ptr, s.ptr, size) после проверки этого размера! = 0. И конструктор по умолчанию должен делать memset (ptr, 0, size), если size! = 0.
kurt Крюкеберг
Хорошо спасибо. Таким образом, этот комментарий и два предыдущих комментария могут быть удалены, поскольку проблема также устранена в ответе.
К.Карамазен