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

603

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

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

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

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

  • 45
    Я добавил это в c ++ - faq, так как я уверен, что это произойдет в будущем.
  • 2
    связанный вопрос о семантике перемещения
Показать ещё 6 комментариев
Теги:
c++11
rvalue-reference
perfect-forwarding
c++-faq

4 ответа

512
Лучший ответ

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

Здесь приведено введение в rvalue ссылки.

Здесь фантастический глубокий взгляд на ссылки rvalue одной из стандартных библиотек Microsoft . (Но см. Предупреждение в комментариях, следующих за этим ответом, перед тем, как прочитать эту статью.)

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

T&& r = T();

Ссылки rvalue в первую очередь предусматривают следующее:

Переместить семантику. Оператор присваивания шага конструктора и двигаться теперь может быть определен, что принимает ссылку RValue вместо обычного сопзЬ-именующей ссылки. Движение функционирует подобно копиям, за исключением того, что он не обязан сохранять исходный код без изменений; на самом деле, он обычно изменяет источник таким образом, что он больше не владеет перемещенными ресурсами. Это отлично подходит для устранения посторонних копий, особенно в стандартных реализациях библиотек.

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

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

Если этот конструктор был передан временным, копия была бы ненужной, потому что мы знаем, что временное будет просто уничтожено; почему бы не использовать ресурсы, выделенные временным образом? В С++ 03 нет способа предотвратить копирование, поскольку мы не можем определить, что мы прошли временный. В С++ 11 мы можем перегрузить конструктор перемещения:

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

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

Конструктор перемещения будет использоваться для временных и для не-const 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 позволяют нам правильно пересылать аргументы для шаблонных функций. Возьмем, к примеру, эту функцию factory:

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 принимает аргумент конструктора посредством ссылки не const? Чтобы создать действительно общую функцию factory, нам пришлось бы перегружать factory на A1& и на A1 const&. Это может быть хорошо, если factory принимает один тип параметра, но каждый дополнительный тип параметра будет умножать необходимую перегрузку, установленную на 2. Это очень быстро не поддается.

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

Это позволяет нам определить функцию factory следующим образом:

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

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

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

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

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

  • Для разрешения перегрузки lvalues ​​предпочитают привязку к lvalue-ссылкам, а rvalues ​​предпочитают привязывать к rvalue-ссылкам. Следовательно, почему временные пользователи предпочитают ссылаться на оператор компоновки перемещения/перемещения по оператору присваивания экземпляра/присваивания.
  • Ссылки rvalue будут неявно связываться с rvalues ​​и временными, которые являются результатом неявного преобразования. т.е. float f = 0f; int&& i = f; хорошо сформирован, поскольку float неявно конвертируется в int; ссылка будет связана с временным результатом преобразования.
  • Именованные ссылки rvalue - lvalues. Без названия ссылки rvalues.Это важно понять, почему вызов std::move необходим в: foo&& r = foo(); foo f = std::move(r);
  • 54
    +1 для Named rvalue references are lvalues. Unnamed rvalue references are rvalues. ; не зная этого, я изо всех сил пытался понять, почему люди делают T &&t; std::move(t); в течение длительного времени в движении, и тому подобное.
  • 0
    @MaximYegorushkin: В этом примере r связывается с чистым rvalue (временным) и, следовательно, временный должен иметь расширенную область жизни, не так ли?
Показать ещё 6 комментариев
79

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

В С++ 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&);

В С++ 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 может быть юридически сохранен в стандартных контейнерах, отсортирован и т.д., тогда как С++ 03 std::auto_ptr не может.

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

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

Не могу вспомнить, что говорит об этом С++ 03, но в С++ 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. В С++ 03 вы должны перегружать все четыре комбинации констант и не константы вручную. В С++ 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 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.

  • 0
    почему вы использовали std::forward<A>(aref) < std::forward<B>(bref) ? и я не думаю, что это определение будет правильным, когда вы попробуете перехватить int& и float& . Лучше оставить один тип шаблона формы.
18

Термин T&& при использовании с выводом типа (например, для идеальной пересылки) известен в разговорной речи как универсальная ссылка. Это было придумано Скоттом Мейерсом в этой статье.

Это потому, что это может быть либо r-значение, либо l-значение.

Примеры:

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

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

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

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

Обратите внимание, что сам стандарт не имеет понятия об этом, это просто способ обсуждать (нечетную?) комбинацию правил сбрасывания ссылок, выведения ссылочного типа и && & синтаксис.

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

7

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

Итак, что такое rvalue? Все, что не является lvalue. Lvalue является выражением, которое относится к ячейке памяти и позволяет нам адресовать адрес этой ячейки памяти с помощью оператора &.

Сначала легче понять, какие результаты выполняются с помощью примера:

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

Отличная статья, объясняющая все это и многое другое (например, как rvalues позволяют совершенную переадресацию и что это означает) с большим количеством хороших примеров - Thomas Becker C++ Rvalue References Explained. Этот пост в значительной степени опирался на его статью.

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню