«Универсальные» ссылки в C++11 или T&& не всегда означает «Rvalue Reference»

Не так давно Скотт Майерс (англ. Scott Meyers) — эксперт по языку программирования C++, автор многих известных книг — опубликовал статью, описывающую подробности использования rvalue ссылок в C++11.
На Хабре эта тема еще не поднималась, и как мне кажется, статья будет интересна сообществу.
Оригинал статьи: «Universal References in C++11—Scott Meyers»

«Универсальные» ссылки в C++11


T&& не всегда означает “Rvalue Reference”

Автор: Scott Meyers

Возможно, наиболее важным нововведением в C++11 являются rvalue ссылки. Они служат тем фундаментом, на котором строятся «семантика переноса (англ. move semantics)» и «perfect forwarding». (Вы можете ознакомится с основами данных механизмов в обзоре Thomas’а Becker’а).

Синтаксически rvalue ссылки объявляются также, как и «нормальные» ссылки (теперь называемые lvalue ссылками), за исключением того, что вы используете два амперсанда вместо одного. Таким образом, эта функция принимает параметр типа rvalue-reference-to-Widget:
void f(Widget&& param);

Учитывая, что rvalue ссылки объявляются с помощью “&&”, было бы разумно предположить, что присутствие “&&” в объявлении типа указывает на rvalue ссылку. Но это не так:
Widget&& var1 = someWidget;         // здесь “&&” означает rvalue ссылку

auto&& var2 = var1;                 // здесь “&&” НЕ означает rvalue ссылку

template<typename T>
void f(std::vector<T>&& param);     // здесь “&&” означает rvalue ссылку

template<typename T>
void f(T&& param);                  // здесь “&&” НЕ означает rvalue ссылку

В этой статье я опишу два значения “&&” в объявлении типа, разъясню как отличить их друг от друга и введу новую терминологию, которая позволит однозначно определять какое значение “&&” используется. Выделение разных значений важно, потому что если Вы думаете про «rvalue ссылку» когда видите “&&” в объявлении типа, Вы неправильно поймете большое количество C++11 кода.

Суть вопроса в том, что “&&” в объявлении типа означает rvalue ссылку, но иногда это же может означать либо rvalue ссылку, либо lvalue ссылку. Таким образом, в некоторых случаях “&&” в исходном коде может иметь значение “&”, т.е. синтаксически иметь вид rvalue ссылки (“&&”), а в действительности быть lvalue ссылкой (“&”).

Ссылки являются более гибким понятием, чем lvalue ссылки или rvalue ссылки. Так rvalue ссылки могут быть связаны только с rvalue, а lvalue ссылки, в добавление к возможности привязки к lvalue, могут быть связаны с rvalue при ограниченных условиях (ограничения на связывание lvalue ссылок и rvalue заключается в том, что такое связывание допустимо только тогда, когда lvalue ссылка объявлена как ссылка на константу, т.е. const T&.) Ссылки же, объявленные с “&&”, которые могут быть либо lvalue ссылками, либо rvalue ссылками, могут быть связаны с чем угодно. Такие необычно гибкие ссылки заслуживают своего названия. Я назвал их «универсальными» ссылками.

Подробности, когда “&&” означает универсальную ссылку (т.е. когда “&&” в исходном коде может реально означать “&”) достаточно сложны, так что я отложу их описание. А сейчас давайте сосредоточимся на следующем правиле, потому что это то, что Вы должны помнить при ежедневном программировании:

Если переменная или параметр объявлены с типом T&& для некоторого выводимого типа T, такая переменная или параметр является универсальной ссылкой.

Требование выведения типа ограничивает круг ситуаций, где могут быть универсальные ссылки. Практически, почти все универсальные ссылки — это параметры шаблонов функций. И поскольку правила выведения типа для авто-объявляемых переменных в основном те же, что и для шаблонов, возможны авто-объявленные универсальные ссылки. Они не часто встречаются в продакшн коде, но я приведу некоторые в этой статье, поскольку они менее многословны примеров с шаблонами. В разделе «Мелкие детали» данной статьи я покажу возможность возникновения универсальных ссылок в связи с использованием typedef и decltype, но пока мы не добрались до «Мелких деталей», я буду исходить из того, что универсальные ссылки относятся только к шаблонам функций и авто-объявленным переменным.

Форма декларации универсальной ссылки T&& является более значимым требованием, чем это может показаться, но я вернусь к рассмотрению данного вопроса позже. А пока, просто имейте в виду это требование.

Как и все ссылки, универсальные ссылки должны быть инициализированы, и именно инициализатор универсальной ссылки определяет будет ли она представлять из себя lvalue ссылку или rvalue ссылку:
  • Если выражение, которым инициализируется универсальная ссылка является lvalue, то универсальная ссылка становиться lvalue ссылкой.
  • Если выражение, которым инициализируется универсальная ссылка является rvalue, то универсальная ссылка становиться rvalue ссылкой.

Эта информация полезна только в том случае, если Вы в состоянии отличить lvalue от rvalue. Точное определение этих терминов сложно выработать (С++11 стандарт дает общее определение того, является выражение lvalue или rvalue от случая к случаю), но на практике хватает следующего:
  • Если можно взять адрес выражения, то это выражение lvalue.
  • Если тип выражения является lvalue ссылкой (т.е. T& или const T&, и т.п.), то это выражение lvalue.
  • В противном случае выражение является rvalue. Концептуально (и, как правило, на самом деле), rvalue соответствуют временным объектам, таким как возвращаемым из функций или созданным путем неявного преобразования типов. Большинство литералов (например, 10 и 5.3), также rvalue.

Посмотрим еще раз на код из начала статьи:
Widget&& var1 = someWidget;
auto&& var2 = var1;

Вы можете взять адрес var1, соответственно var1 — это lvalue. Объявление типа var2 как auto&& делает var2 универсальной ссылкой, и так как она инициализируется var1 (lvalue), var2 становится lvalue ссылкой.

Небрежное чтение исходного кода может заставить Вас поверить, что var2 является rvalue ссылкой; “&&” в объявлении, конечно, наводит на эту мысль. Но так как var2 — универсальная ссылка, инициализированная lvalue, она является lvalue ссылкой. Это как если бы var2 была объявлена следующим образом:
Widget& var2 = var1;

Как отмечалось выше, если выражение имеет тип lvalue ссылки, это lvalue. Рассмотрим такой пример:
std::vector<int> v;
...
auto&& val = v[0];	// val становится lvalue ссылкой (см. ниже)

val является универсальной ссылкой и инициализирована v[0], т.е. результатом вызова std::vector<int>::operator[]. Эта функция возвращает lvalue ссылку на элемент vector (я игнорирую выход за пределы массива, что приведет к неопределенному поведению).

Так как все lvalue ссылки являются lvalue, и так как это lvalue используется для инициализации val, val становится lvalue ссылкой, хотя объявление типа val выглядит как rvalue ссылка.

Я отмечал, что универсальные ссылки наиболее распространены в параметрах шаблонов функций. Рассмотрим еще раз шаблон из начала этой статьи.
template<typename T>
void f(T&& param);	// “&&” может означать rvalue ссылку

При таком вызове f,
f(10);	// 10 является rvalue

param инициализирован литералом 10, который, по той причине, что нельзя взять его адрес, является rvalue. Это означает что в вызове f универсальная ссылка param инициализирована rvalue и, таким образом, становится rvalue ссылкой -–в частности int&&.

С другой стороны, если f вызывается как то так:
int x = 10;
f(x);		// x является lvalue

param инициализирован переменной x, которая, по той причине, что можно взять ее адрес, является lvalue. Это означает, что в данном вызове f универсальная ссылка param инициализирована lvalue, и param поэтому становится lvalue ссылкой -– int&, если быть точным.

Комментарий рядом с объявлением f теперь должен быть понятен: будет тип param lvalue ссылкой или rvalue ссылкой зависит от того, что было передано в f при вызове. Иногда param становится lvalue ссылкой, а иногда rvalue ссылкой. То есть param действительно является универсальной ссылкой.

Помните, что “&&” обозначает универсальную ссылку только тогда, когда имеет место выведение типа. Там, где нет выведения типа, нет и универсальной ссылки. В таких случаях “&&” в объявлении типа всегда означает rvalue ссылку. Следовательно:
template<typename T>
void f(T&& param);         // выведенный тип параметра ⇒ выведение типа; && ≡ универсальная ссылка

template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);   // полностью определенный тип параметра ⇒ нет выведение типа;
    ...                     // && ≡ rvalue ссылка
};
 
template<typename T1>
class Gadget {
    ...
    template<typename T2>
    Gadget(T2&& rhs);        // выведенный тип параметра ⇒ выведение типа; && ≡ универсальная ссылка
};

void f(Widget&& param);      // полностью определенный тип параметра ⇒ нет выведение типа;
                             // && ≡ rvalue ссылка

Нет ничего удивительного в этих примерах. В любом случае, если Вы видите T&& (где T — это параметр шаблона), присутствует выведение типа, поэтому Вы смотрите на универсальную ссылку. А если Вы видите “&&” после определенного имени типа (например, Widget&&), Вы смотрите на rvalue ссылку.

Я заявил, что форма декларации ссылки должна быть «T &&» для того, чтобы ссылка была универсальной. Это важный нюанс. Посмотрите еще раз на декларацию из начала этой статьи:
template<typename T>
void f(std::vector<T>&& param);  // “&&” означает rvalue ссылку

Здесь у нас есть и выведение типа и “&&”-описанный параметр функции, но форма декларации параметра не “T&&”, а “std::vector<T>&&”. В результате параметр является нормальной rvalue ссылкой, а не универсальной ссылкой. Объявление универсальной ссылки может быть только в форме “T&&”! Даже простого добавление const спецификатора достаточно, чтобы не интерпретировать “&&” как универсальную ссылку.
template<typename T>
void f(const T&& param);	// “&&” означает rvalue ссылку

“T&&” является просто необходимой формой для объявления универсальных ссылок. Это не значит, что Вы должны использовать имя T для параметров шаблона.
template<typename MyTemplateParamType>
void f(MyTemplateParamType&& param);  // “&&” означает универсальную ссылку

Иногда вы можете увидеть T&& в декларации функции шаблона, где T является параметром шаблона, но пока еще нет выведения типа. Рассмотрим функцию push_back в std::vector (показана только интересующая нас версия
std::vector::push_back):
template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    void push_back(T&& x);	// полностью определенный тип параметра ⇒ нет выведение типа;
    ...				// && ≡ rvalue ссылка
};

Здесь T является параметром шаблона, и push_back принимает T&&. Тем не менее параметр не является универсальной ссылкой! Как это может быть?

Ответ становится очевидным, если мы посмотрим на то, как push_back будет объявлена вне класса. Я буду делать вид, что параметр Allocator отсутствует, чтобы не загромождать код. Учитывая это, ниже приводится декларация этой версии
std::vector::push_back:
template <class T>
void vector<T>::push_back(T&& x);

push_back не может существовать без класса std::vector<T>, который его содержит. Но если у нас есть класс std::vector<T>, то мы уже знаем чем является T, и таким образом, нет необходимости выводить этот тип.

Посмотрим пример. Если я напишу,
Widget makeWidget(); 		// фабричная функция для Widget
std::vector<Widget> vw;
...
Widget w;
vw.push_back(makeWidget());	// создает Widget и добавляет в vw

то мое использование push_pack скажет компилятору инстанцировать эту функцию для класса std::vector<Widget>. Ее декларация вне класса будет выглядеть так:
void std::vector<Widget>::push_back(Widget&& x);

Понимаете? Как только мы знаем, что класс — это std::vector<Widget>, тип параметра push_back полностью определен. Выведения типа не производится.
Сравните это с методом emplace_back std::vector'а, которая объявлена следующем образом:
template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    template <class... Args>
    void emplace_back(Args&&... args);  // выведенный тип параметра ⇒ выведение типа;
    ...                                 // && ≡ универсальная ссылка
};

Не обращайте внимание на то, что emplace_back принимает переменное число аргументов (как указано в декларации Args и args). Здесь важно то, что типы для каждого из аргументов должны быть выведены. Параметр шаблона функции Args не зависит от параметра шаблона класса T, таким образом, даже если класс полностью известен, скажем std::vector<Widget>, это ничего не говорит о типе (типах) аргументов emplace_back. Объявление emplace_back вне класса для std::vector<Widget> явно это показывает (я продолжаю игнорировать существование параметра Allocator):
template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);

Очевидно, знание того, что класс — это std::vector< Widget >, не устраняет необходимости вывода типа (типов), передаваемых в emplace_back. В результате параметры std::vector::emplace_back являются универсальными ссылками в отличии от параметра версии std::vector::push_back, который, как мы увидели, является rvalue ссылкой.

Следует иметь в виду то, что является ли выражение lvalue или rvalue не зависит от его типа. Рассмотрим тип int. Есть lvalue типа int (например, переменные, объявленные int) и есть rvalue типа int (например литералы, например, 10). Это справедливо и для пользовательских типов, как Widget. Объект Widget может быть lvalue (например, переменная Widget) или rvalue (например, функция-фабрика вернула созданный Widget объект). Тип выражения не скажет Вам, является ли это lvalue или rvalue.
Widget makeWidget();                 // фабричная функция для Widget
Widget&& var1 = makeWidget();        // var1 является lvalue, но тип
                                     // var1 – это rvalue ссылка (на Widget)
Widget var2 = static_cast< Widget&& >(var1);
                                     // cast выражение дает rvalue, но
                                     // его тип - это rvalue ссылка (на Widget)

Общепринятым способом превратить lvalue (например var1) в rvalue является использование std::move, так var2 может быть определена следующим образом:
Widget var2 = std::move(var1);	// эквивалентно коду выше

Я изначально привел код с использованием static_cast только для того, чтобы явно показать, что типом выражения является rvalue ссылка (Widget&&).

Именованные переменные и параметры типа rvalue ссылка являются lvalue. (Вы можете получить их адрес.) Еще раз рассмотрим Widget и Gadget шаблоны:
template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // тип rhs - rvalue ссылка,
    ...                          // но rhs является lvalue
};
 
template<typename T1>
class Gadget {
    ...
    template <typename T2>
    Gadget(T2&& rhs);// rhs является универсальной ссылкой чей тип
    ...              // в конечном итоге станет rvalue ссылкой или
};                   // lvalue ссылкой, но rhs является lvalue

В конструкторе Widget rhs является rvalue ссылкой, так что мы знаем, что оно связано с rvalue (т.е. было передано rvalue), но само rhs является lvalue, поэтому мы должны преобразовать его обратно в rvalue, если мы хотим получить преимущества от того, что rhs связано с rvalue. Наше желание, как правило, обусловлено требованием использовать rhs в качестве источника переноса, поэтому для преобразования lvalue в rvalue применяется std::move. Подобным образом rhs в конструкторе Gadget является универсальной ссылкой, и следовательно оно может быть связано с lvalue или с rvalue, но в любом случае само rhs является lvalue. Если оно связано с rvalue и мы хотим получить преимущества от этого, мы должны преобразовать rhs обратно в rvalue. Однако, если оно связано с lvalue, мы конечно не хотим трактовать его как rvalue. Такая зависимость от того, с чем связана универсальная ссылка, служит причиной для использования std::forward: взять универсальную ссылку и преобразовать ее в rvalue только в том случае, если она связана с rvalue выражением. Название функции (“forward”) подтверждает наши ожидания, что она выполнит пересылку в другую функцию, всегда сохраняя тип ссылки аргумента (lvalue или rvalue).

Но std::move и std::forward не являются предметом данной статьи. Статья повествует о том факте, что "&&" в объявления типа может или не может описывать rvalue ссылку. Чтобы не отвлекаться я отсылаю Вас к ссылкам в разделе «Дополнительная информация» для подробного описания std::move и std::forward.

Мелкие детали

Суть вопроса в том, что некоторые конструкции в C++11 порождают ссылки на ссылки, а ссылки на ссылки не допускаются в C++. Если исходный код явно содержит ссылку на ссылку — код не верен:
Widget w1;
...
Widget&  & w2 = w1;	// Ошибка! Нет такого понятия как “ссылка на ссылку”

Однако есть случаи, где ссылки на ссылки возникают в результате манипуляций с типами, которые происходят во время компиляции, и в таких случаях, отвергнуть этот код будет проблематично. Мы это знаем из опыта первоначального стандарта для C++, т.е., C++98/C++03.

Во время выведения типа для параметров шаблона, который является универсальной ссылкой, lvalue и rvalue одного типа выводятся в несколько различных типов. В частности, lvalue типа T выводятся как тип T& (т.е. lvalue ссылка на T), а rvalue типа T выводятся просто как тип T. (Обратите внимание, что lvalue выводится как lvalue ссылка, rvalue не выводятся как rvalue ссылка!) Рассмотрим что происходит при вызове шаблонной функции, принимающей универсальную ссылку, с rvalue и lvalue:
template<typename T>
void f(T&& param);
...
int x;
...
f(10);         // вызывается f с rvalue
f(x);          // вызывается f с lvalue

В вызове f с rvalue 10 T выводится как int и интстансирование f выглядит так:
void f(int&& param);	// f инстанцированная из rvalue

Это хорошо. Однако в вызове f с lvalue x, T выводится как int&, и интстансирование f содержит ссылку на ссылку:
void f(int& && param);	// первоначальное инстанцирование f с lvalue

Из-за ссылки на ссылку этот код экземпляра выглядит на первый взгляд неверным, но исходный код “f(x)” – это вполне разумно. Чтобы не отвергать этот код, C++ выполняет “свертывание ссылок” когда возникает ссылка на ссылку в контекстах, таких как инстанцирование шаблона.

Так как есть два вида ссылок (lvalue ссылки и rvalue ссылки) существует четыре возможных комбинации ссылки на ссылку: lvalue ссылка на lvalue ссылку, lvalue ссылка на rvalue ссылку, rvalue ссылка на lvalue ссылку и rvalue ссылка на rvalue ссылку. Есть только два правила свертывания ссылок:
  • Rvalue ссылка на rvalue ссылку становится (“сворачивается в”) rvalue ссылкой.
  • Все остальные ссылки на ссылки (т.е. все комбинации с участием lvalue ссылки) сворачиваются в lvalue ссылку.

Применение этих правил к инстанцированию f с lvalue дает следующий правильный код:
void f(int& param);  // инстанцирование f с lvalue после свертывания ссылок

Это дает точный механизм, которым универсальная ссылка может (после выведения типа и свертывания ссылок) быть превращена в lvalue ссылку. В действительности универсальная ссылка — это просто rvalue ссылка в контексте свертывания ссылок.

Особая ситуация, когда выводится тип для переменных, которые являются ссылками. В таком случае, часть типа, обозначающая ссылку, игнорируется. Например, если
int x;
...
int&& r1 = 10;	// тип r1 - int&&
int& r2 = x;	// тип r2 - int&

то тип как для r1, так и для r2 при вызове шаблона f считается int. Такое поведение отбрасывания ссылок не зависит от правил выведения типа для универсальных типов, lvalue выводятся как тип T&, а rvalue как тип T, и таким образом в этих вызовах,
f(r1); 
f(r2);

выведенный тип как для r1, так и для r2 будет int&. Почему? Во-первых, ссылочная часть типов r1 и r2 отбрасывается (получается int в обоих случаях), затем, так как это lvalue, оба рассматриваются как int& во время выведения типа для параметра-универсальной ссылки в вызове f.

Свертывание ссылок происходит, как я отметил, в “контекстах, таких как инстанцирование шаблона”. Второй такой контекст – это определение “auto” переменных. Выведение типа для auto переменных, которые являются универсальными ссылками, по сути идентично выведению типа для параметров шаблонов функций, которые являются универсальными ссылками, так lvalue типа T выводится как имеющее тип T&, а rvalue типа T выводится как имеющее тип T&. Рассмотрим еще раз пример из начала статьи:
Widget&& var1 = someWidget; // var1 имеет тип Widget&& (auto не используется)
auto&& var2 = var1;	    // var2 имеет тип Widget& (см. ниже)

Тип var1 — Widget&&, но его “ссылочная часть” игнорируется во время выведения типа при инициализации var2; он считается типом Widget. Так как это lvalue, которое используется для инициализации универсальной ссылки (var2), выведенный тип будет Widget&. Подставляя Widget& вместо auto в определении var2, получим следующий неверный код,
Widget& && var2 = var1;		// обратите внимание на ссылку на ссылку

который после сворачивания ссылок станет
Widget& var2 = var1;		// var2 имеет тип Widget&

Третий контекст сворачивания ссылок – это формирование и использование typedef. Учитывая этот шаблон класса
template<typename T>
class Widget {
    typedef T& LvalueRefType;
    ...
};

и такое использование этого шаблона,
Widget<int&> w;

инстанцированный класс будет содержать такой (неверный) typedef:
typedef int& & LvalueRefType;

Сворачивание ссылок приводит к следующему верному коду:
typedef int& LvalueRefType;

Если мы затем будем использовать этот typedef в контексте с использованием ссылок на него, например,
void f(Widget<int&>::LvalueRefType&& param);

после развертывания typedef будет создан следующий неверный код,
void f(int& && param);

но сворачивание ссылок урежет его и окончательное объявление f будет:
void f(int& param);

Последний контекст, где применяется сворачивание ссылок – это использование decltype. Как и в случаях с шаблонами и auto, decltype выполняет выведение типа выражения, которое дает типы либо T, либо T&, и decltype затем применяет правила сворачивания ссылок C++11.

К сожалению, правила сворачивания ссылок, применяемые decltype не те, что используются при выведении типа для шаблона или auto типа. Подробности слишком сложны для обсуждения здесь (в разделе «Дополнительная информация» приводятся ссылки для деталей), но заметная разница в том, что decltype для именованной переменной не ссылочного типа выводит тип T (т.е. не ссылочный тип), когда при некоторых условиях шаблоны и auto-типы выводят тип T&. Другое важное различие в том, что выведение типа decltype зависит только от decltype выражения; тип инициализирующего выражения (если оно есть) игнорируется. Следовательно:
Widget w1, w2;
auto&& v1 = w1;	// v1 является универсальной ссылкой,
                // инициализированной lvalue, соответственно v1
                // является lvalue ссылкой на w1.
 
decltype(w1)&& v2 = w2; // v2 является универсальной ссылкой, и decltype(w1) это Widget, 
                        // таким образом v2 является rvalue ссылкой.
                        // w2 это lvalue, и недопустимо инициализировать
                        // rvalue ссылку lvalue, таким образом код не будет компилироваться.


Заключение

В описании типа “&&” означает либо rvalue ссылку, либо универсальную ссылку – ссылку, которая быть или lvalue ссылкой или rvalue ссылкой. Универсальные ссылки всегда имеют форму T&& для некоторого выведенного типа T.

Сворачивание ссылок – это механизм приведения универсальных ссылок (которые являются просто rvalue ссылками в ситуациях, когда применяется сворачивание ссылок) иногда к lvalue ссылкам, а иногда к rvalue ссылкам. Он используется в специальных контекстах, в которых в результате компиляции могут появиться ссылки на ссылки. Это контексты выведения типа шаблона, выведения auto-типа, формирования и использования typedef и выражения decltype.

Благодарности

Черновые версии данной статьи рецензировались Cassio Neri, Michal Mocny, Howard Hinnant, Andrei Alexandrescu, Stephan T. Lavavej, Roger Orr, Chris Oldwood, Jonathan Wakely и Anthony Williams. Их замечания способствовали существенным улучшениям статьи, а также ее презентации.

Дополнительная информация

C++11, Wikipedia.

Overview of the New C++ (C++11), Scott Meyers, Artima Press, last updated January 2012.

C++ Rvalue References Explained, Thomas Becker, last updated September 2011.

decltype, Wikipedia.

“A Note About decltype,” Andrew Koenig, Dr. Dobb’s, 27 July 2011.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 68

    +18
    Кажется у меня случился перелом мозга после этой статьи…
      +1
      c++ он такой… для избранных фанатов извращений)
        +9
        Если Вам в C++ не нравится какой-то механизм — не используйте его, только и всего.

        Никто не заставляет Вас пользоваться rValue-ссылками — можете использовать обычные lValue.
        Более того, никто не заставляет пользоваться ссылками вообще — можете везде юзать указатели, как в старом добром С.

        rValue-ссылки — это прекрасный способ полноценного использования временного объекта. Благодаря нему, в частности, в С++11 инициализация STL-ных контейнеров происходит куда быстрее, нежели в старом C++03 — поскольку для объектов, «заполняющих» контейнер, теперь вызывается не контруктор копирования, а констуктор перемещения.

        А если в других языках подобных «извращений» нету — то это вовсе не значит, что эти языки лучше С++. Это значит лишь то, что С++ даёт программисту больший контроль над ситуацией, чем эти языки.
          0
          Хм. А если достался чужой проект в наследство или аутсорс какой-то. Что значит не используйте?
            –1
            Возможно пора менять работу. Или даже профессию. На что нибудь более простое и ненапряжное.
      –4
      Мне кажется, или C++ действительно на всех парах мчится к хаосу?! ИМХО, большинство новшеств C++xx добавляют больше беспорядка в синтаксис, некоторые из них выглядят чужеродно. Такими темпами в стандарте С++20 черт ногу сломит. При написании программ, я стараюсь использовать возможности C++xx по минимуму, для улучшения читаемости кода.
        +6
        На самом деле всё довольно просто; просто данная статья Скота не очень удачная, он чересчур сложно объяснил вещи, которые можно было прям выдрать из стандарта и всё бы встало на свои места.
          –1
          Данная мысль у меня возникла уже довольно давно. С С++ я имею дело аж с 9 класса школы, т.е. уже почти 10 лет. Стандарты C++xx в некоторых случаях подобны снежному кому, который разрастается с каждым новым C++xx. В связи с этим 2 года назад я перешел на использование Python, C# и Java вместо C++. Они тоже не идеальны, но «скользких мест» в них меньше. В 2006 году появилась книга которая так и называлась «Скользкие места С++. Как избежать проблемы при проектировании и компиляции ваших программ», и это не единственная книга в своем роде. Думаю, если все так и будет продолжаться, то можно будет увидеть в продаже книги: «Изучение C++xx или как не заработать рак мозгов», «Трехтомник: новый стандарт С++хх», «Лечение геморроя или учимся читать код С++хх»… Да и сам синтаксис стал уродливее в C++xx.
            +10
            Я постоянно пишу на C++ и могу сказать, что язык становится проще. Да появляются новые вещи, которые лучше бы знать. Но очень многое из этого не будет применяться большим числом С++ программистов-прикладников, т.к… многое из нововведений необходимо для написания кода библиотек.
              +3
              Вот вот, просто если не использовать старый deprecated хлам, а писать на С++11, то код получается проще, лаконичнее и понятнее, а главное не становится медленее.
                0
                +1
                +2
                Какие «стандарты»? 10 лет назад был 1998 стандарт, сейчас 2011, больше стандартов нет
                  +1
                  А 2003й?
                    +1
                    2003 был как дополнение(исправление ошибок). Формально стандарта 2 — 98 и 11. Следующий будет в 17-м, минорный в 14-м.
              +2
              При написании программ, я стараюсь использовать возможности C++xx по минимуму, для улучшения читаемости кода

              Ну почему же, в C++11 есть чудесные, с моей точки зрения, вещи. Хотя бы auto, decltype, constexpr.
                +4
                И лямбды, лямбды :) Моя любимая возможность из C++11, которая используется у нас уже буквально по всему проекту и некоторые вещи значительно упростила. Ну и range-based for, конечно. Хотя мне как в первую очередь Qt-разработчику вполне хватало костыля в виде Qt-шного foreach.

                В общем, масса полезных и удобных вещей в новом стандарте добавилась. Хотя не спорю, что rValue references как и вообще любая семантика переноса может с непривычки снести мозг даже опытным разработчикам.
                  0
                  С++11 и его новые возможности — это как новый ящик инструментов. Можно быстрее гвозди забивать, а можно и руку поранить. Дело в сноровке. :)
                0
                Интересная конструкция. С передачей && в функцию, вроде бы, понятно. А вот с возвратом из нее — не очень. Когда в C++ появился тип T&, с ним было понятно: он эквивалентен T const *, только записывается по-другому, и в переменной T&x в действительности лежит указатель. А что лежит в переменной T&& x? И что возвращается из функции T&& foo()?
                  +2
                  Возвращается «висячая ссылка». Как и с lvalue ref это «запрещенный» приём. Можете думать об rvalue ref так же как и lvalue ref, в плане хранения. Вся разница между ними в семантике применения.
                    0
                    Ссылка на что? Кто предоставляет место для объекта, на который идёт ссылка — вызывающая функция, как в случае возврата по значению? Или «возможны варианты»?

                    И если уж речь зашла о семантике — какова вообще семантика кода
                    Widget&& var1 = someWidget;
                    

                    Может ли, например, someWidget быть переменной (я так понял, что нет — в этом случае у него есть имя, а значит, он lvalue), и если да — какова его судьба после этой строчки? А если он не переменная — чем эта строчка вообще будет отличаться от
                    Widget var1 = someWidget;
                    

                    по реализации и по семантике?
                      0
                      Если Вы возвращаете(T&& foo()) ссылку, тогда она указывает на объект, который находится внутри стека функции, а следовательно она «висит». Т.е. разницы с lvalue ref нет. Где хранится объект? Насколько я понимаю, если создается rvalue ref(int&& var1 = 5;) то будет создан объект на стеке, на который будет указывать ссылка. Т.е. опять никаких отличий. А вот теперь семантика: Ваш пример(Widget&& var1 = someWidget;) не скомпилируется, т.к. нельзя привязать rvalue ref к lvalue, но если немного модифицировать: Widget&& var1 = Widget(someWidget); или Widget&& var1 = std::move(someWidget); то всё будет хорошо(смотри предыдущее предложение)
                        0
                        то будет создан объект на стеке, на который будет указывать ссылка — то есть, место предоставляет вызвающая функция, и за последующее уничтожение объекта отвечает она же. Это понятно.

                        Widget&& var1 = someWidget1+someWidget2;
                        

                        — то же самое, что
                        Widget _var1 = someWidget1+someWidget2;
                        Widget* var1=&_var1?
                        

                        (с разрешением тем, кто использует var1, портить его содержимое)? Правильно?

                        А что случится с кодом
                        Widget&& var1 = static_cast<Widget&&>(someWidget);
                        Widget var2,var3;
                        var2=var1;
                        var3=var1;
                        

                        (при условие, что operator=(Widget&&) деструктивен)? Второе присваивание будет некорректным?

                        И еще — внутри функции foo(T &&x) переменная x является lvalue, т.е. можно смело писать a=x; b=x;?

                        Начинаю понимать. Когда мы в детстве писали полиномиальную арифметику на списках (еще на C), то в функцию сложения приходилось передавать флажок — можно ли разрушать каждый из аргументов, или нужно создавать его копию. Похоже, что && является признаком «разрушать можно»…

                        Есть ли хоть один пример, когда действительно имеет смысл писать T&& x=… (а не использовать std::move() в нужном месте несколько позже)?

                          +2
                          Widget _var1 = someWidget1+someWidget2; //Будет вызван move-конструктор(если он есть)
                          Widget&& var1 = someWidget1+someWidget2; //На стеке будет создан объект, посредством move ctor(если он есть), после чего на этот объект будет создана ссылка.

                          Если move ctor отсутствует, значит будет вызван copy ctor.

                          Widget&& var1 = static_cast<Widget&&>(someWidget);//создаём объект на стеке, воруя содержимое someWidget. someWidget нельзя использовать более
                          Widget var2,var3;//default ctor
                          var2=var1;//operator=(const Widget& rhs)
                          var3=var1;//operator=(const Widget& rhs)

                          Т.е. в последних двух строчках происходит обычное копирование. А вот есл написать так:
                          var2=std::move(var1);//operator=(Widget&& rhs)
                          var3=var1;//Уупс

                          И еще — внутри функции foo(T &&x) переменная x является lvalue, т.е. можно смело писать a=x; b=x;?

                          Да, любое выражение, которое имеет имя есть lvalue.
                          Похоже, что && является признаком «разрушать можно»…

                          Именно.
                          Есть ли хоть один пример, когда действительно имеет смысл писать

                          У меня нет.
                            0
                            var2=var1;//operator(const Widget& rhs)

                            Этого места я не понял. Почему вызовется operator(const Widget& rhs), а не operator(Widget&& rhs)? Как тогда можно использовать lvalueness объекта var1, не прибегая к дополнительному std::move()?
                              0
                              Потому, что var1 есть lvalue типа Widget&&. Для lvalue всегда вызывается оператор копирования
                                0
                                А как мне добраться до его rvalue-содержимого?
                                  +1
                                  Вы не понимаете. Нет никакого rvalue содержимого. Rvalue&rvalue есть лишь свойство выражения. Чтобы вызвать move семантику, нужно использовать rvalue выражение, для этого применяется, в частности, std::move(или как вы делали static_cast). Посмотрите мою статью. Может прояснит немного.
                                    0
                                    Посмотрел. И вообще перестал понимать разницу между T&& x=… и T x=… — в обоих случаях программе приходится выделять место на rvalue, и в обоих случаях она не может по своей воле использовать его как xvalue… Только в T&& x зачем-то требуется лишний указатель (и то не факт — адрес места под значение, на которое «указывает» x, она и так знает).
                                      0
                                      Разница в типе, выражение идентично.
                                        0
                                        Есть ли код, который после этой строчки будет вести себя по-разному (или в одном случае компилироваться, а в другом — нет)?
                                          0
                                          Разница лишь в том, что один объект ссылка, а второй нет. Со всеми вытекающими.
                                          #include <iostream>
                                          
                                          class A{
                                          public:
                                              virtual void foo()
                                              {
                                                  std::cout << "A\n";
                                              }
                                          };
                                          
                                          class B: public A
                                          {
                                          public:
                                              void foo() override
                                              {
                                                  std::cout << "B\n";
                                              }
                                          };
                                          
                                          int main()
                                          {
                                              A a = B();//bad slicing
                                              A&& b = B();
                                              a.foo();
                                              b.foo();
                                          };
                                          
                                            0
                                            А оно будет работать? Это же я смогу написать

                                            A&& x=f ? A() : B();
                                            x.foo();
                                            

                                            И как бедная программа будет разбираться, какой из временных объектов ей потом удалять?
                                            Но если это предусмотрено — то вот и пример использования. В сочетании с A&& goo(), которая может вернуть любой из потомков А, должно быть вообще шикарно. Но я что-то не верю, что взлетит.
                                              0
                                              То, что я написал — будет. То, что Вы написали в студии выводить 2 раза A, на f true и false. И я не могу сказать, что это не баг студии.
                                                0
                                                А если использовать функцию?
                                                A&& boo(){ return B(); }
                                                
                                                ···
                                                
                                                A&& x=boo();
                                                x.foo();
                                                

                                                Мой прогноз — напишет А. Если вообще скомпилируется.
                                                  0
                                                  Это UB, так как тут «повисшая» ссылка. Нельзя возвращать ссылку на локальный объект…
                                                    0
                                                    Это не локальный объект, это вычисленное выражение, rvalue. Его даже ни одной переменной не присвоили. А на что же еще может возвращать ссылку функция, описанная как A&& f();?
                                                      0
                                                      Это объект созданный в стеке функции. На lvalue, которое может быть уничтожено(так работает move). Нельзя просто так возвращать ссылку. Тут объект уже перестанет существовать, когда функция завершится.
                                                        0
                                                        Допустим, хотя мне это не очевидно (место для этого rvalue могло бы быть выделено вызывающей функцией — так, как это происходит при возврате структуры по значению). Но в таком случае вопрос остается: существует ли хотя бы одна корректная функция, описанная как A&& f()?
                                                          0
                                                          Можете написать. Внутри static A a; И возвращать rvlaue ссылку на него. Понятия не имею зачем, но это можно сделать. Тут те же правила, что и с lvalue ref. Только lvalue никто не «украдёт», когда его вернёшь.
                                                            0
                                                            Всё это очень странно. Получается, что заметная часть статьи (не относящаяся к вызову функций) посвящена конструкции, у которой мало того, что трудно найти практическое использование, так и просто нетривиального применения не видно.
                                                              0
                                                              Применение всегда найдется, а знать как всё работет очень полезно.
                                                              К примеру std::move из студии:
                                                              template<class _Ty> inline
                                                              	typename remove_reference<_Ty>::type&&
                                                              		move(_Ty&& _Arg) _NOEXCEPT
                                                              	{	// forward _Arg as movable
                                                              	return ((typename remove_reference<_Ty>::type&&)_Arg);
                                                              	}
                                                              

                                                              в остальных компиляторах он будет такой же.
                                                                0
                                                                std::move — да, этот приём им нужен. В качестве лазейки из lvalue в rvalue. Кстати, как будет выглядеть её ассемблерный код, если написать её не inline (для конкретного типа) и запретить глобальную оптимизацию? Она просто вернет свой аргумент, или сделает что-нибудь менее тривиальное?
                                                                  0
                                                                  А что там может быть? Move семантика она выше уровнем чем ассемблер.
                                                                    0
                                                                    Действительно, больше ничего. То, что ссылка возвращается на ту же переменную, которую передали параметром, понятно… и далее вся цепочка раскручивается. :(
                                                                0
                                                                struct A {
                                                                	B b;
                                                                	void setB(const B& obj)
                                                                	{
                                                                		b = obj;
                                                                	}
                                                                	void setB(B&& obj)
                                                                	{
                                                                		b = std::move(obj);
                                                                	}
                                                                };
                                                                
                                                                B makeB()
                                                                {
                                                                	return B();
                                                                }
                                                                
                                                                void func()
                                                                {
                                                                	A a;
                                                                	auto&& b = makeB();
                                                                	a.setB(b);
                                                                }
                                                                

                                          0
                                          Просто я действительно не знаю где применять предложенную Вами запись. Обычно rvalue применяется в качестве параметров, а не как объект создаваемый непосредственно в коде.
                                            0
                                            Вытащить ссылку из структуры для удобства?
                                            type&& field = rvaluestruct.field;
                                              0
                                              Интересный вариант. Если мы напишем
                                              F&& x=(A+B).field — сохранится ли A+B? Или нам скажут «низзя» потому что field — это lvalue?
                        0
                        может быть, T& «эквивалентен» все таки T *const, а не T const *?
                          0
                          Да, конечно. Нельзя менять переменную, а не *переменную. Я на С++ последние 10 лет пишу мало, и в тот момент не помнил, куда относится const.
                        +1
                        Хорошая статья, спасибо.
                        Конечно, на первый взгляд все это действо может показаться весьма запутанным, особенно по части самих универсальных ссылок, но на практике все выглядит весьма удобно. В частности, контейнеры без rvalue семантики для меня уже немыслимы. Конечно, есть всякие стандартные и околостандартные трюки вроде RVO, std::swap специализации, но однако же это все лишь обходные средства.
                          +2
                          Я скучаю по временам, когда я программировал на с++. Но вот после таких статей, чувствую облегчение, что завязал.
                            0
                            С move-семантикой и && иногда случаются недоразумения. Первая проблема — это то, что теперь вместо 4-х методов класса по-умолчанию получается 4 + 2 дополнительных (опциональных), но если мы хотим воспользоваться move-семантикой, то должны их объявить. Проблема тут в том, что даже если все члены класса поддерживают move-семантику, такие методы не генерируются компилятором автоматически. Т.е. надо явно написать:

                            class A
                            {
                            public:
                                A(A&& a) = default;
                                void operator=(A&& a) = default;
                            private:
                                std::vector<int> myVec;
                            };
                            

                            Тут спасает, конечно, новые навороты, типа =default, но душу это как-то не сильно греет.

                            Проблема номер 2 связана с тем, что для конкретных типов оптимизация && не применяется. Объясню на примере: предположим, что у нас есть класс A, который требует строку в качестве входного значения:
                            class A
                            {
                            public:
                                A(const std::string& name_) : name(name_) {}
                            private:
                                std::string name;
                            };
                            

                            Если мы вызываем так: A(«my_name»), то тогда создается временный объект std::string, а затем происходит копирование и уничтожение. Казалось бы, тогда надо создавать так:
                            class A
                            {
                            public:
                                A(std::string&& name_) : name(std::move(name_)) {}
                            private:
                                std::string name;
                            };
                            

                            Тогда вышеприведенный пример работает, однако такой — нет:
                            std::string name = "my_name";
                            A a(name);
                            

                            Тогда мы приходим к тому, что надо 2 метода: один с const&, другой — c &&. Уже попахиваем мазохизмом. А если мы добавим в конструктор еще пару полей, типа std::string nick, std::string address, то тогда можно сразу бежать за валерьянкой. Спасает способ, приведенный в статье:
                            class A
                            {
                            public:
                                template<typename T>
                                A(T&& name_) : name(std::forward(name_)) {}
                            private:
                                std::string name;
                            };
                            

                            Но это, как бы так помягче сказать, для особых ценителей искусства. В общем, пока в новом стандарте радуют только лямбды.
                              +2
                              Проблема тут в том, что даже если все члены класса поддерживают move-семантику, такие методы не генерируются компилятором автоматически

                              Не правда. Они генерируется, за исключением:
                              Класс A не содержит явного объявления конструктора копирования
                              Класс A не содержит явного объявления оператора копирования
                              Класс A не содержит явного объявления оператора перемещения
                              Класс A не содержит явного объявления деструктора
                              Конструктора копирования не был явно помечен как deleted
                                0
                                Наверно нечетко выразился. Я имел ввиду не те 4 старых, а 2 новых, с move-семантикой.
                                  0
                                  И я о них(12.8/9 C++11)
                                    0
                                    Можете привести пример, когда они генерятся автоматически? Вот мой пример:

                                    #include <iostream>
                                    
                                    #define F			{std::cout << __PRETTY_FUNCTION__ << std::endl;}
                                    
                                    struct B
                                    {
                                    	B() F
                                    	~B() F
                                    	B(const B&) F
                                    	B(B&&) F
                                    	B& operator=(const B&) F
                                    	B& operator=(B&&) F
                                    };
                                    
                                    struct A
                                    {
                                    	B b;
                                    };
                                    
                                    int main()
                                    {
                                    	A a1;
                                    	A a2(a1);
                                    	A a3(std::move(a2));
                                    	return 0;
                                    }
                                    


                                    Вывод:
                                    B::B()
                                    B::B(const B&)
                                    B::B(const B&)
                                    B::~B()
                                    B::~B()
                                    B::~B()
                                    

                                    Если же реализовать так:
                                    struct A
                                    {
                                    	A()=default;
                                    	A(A&&)=default;
                                    	B b;
                                    };
                                    

                                    То:
                                    B::B()
                                    B::B(const B&)
                                    B::B(B&&)
                                    B::~B()
                                    B::~B()
                                    B::~B()
                                    

                                    Компилятор GCC 4.5.2
                                      +1
                                      GCC 4.5.3
                                      B::B()
                                      B::B(const B&)
                                      B::B(const B&)
                                      B::~B()
                                      B::~B()
                                      B::~B()
                                      

                                      GCC 4.6.3
                                      B::B()
                                      B::B(const B&)
                                      B::B(B&&)
                                      B::~B()
                                      B::~B()
                                      B::~B()
                                      

                                      GCC 4.7.2
                                      B::B()
                                      B::B(const B&)
                                      B::B(B&&)
                                      B::~B()
                                      B::~B()
                                      B::~B()
                                      

                                        0
                                        Ага, пришла пора обновиться. Спасибо за ответ.

                                        Кстати, MSVC 11 CTP NOV12 выдает:
                                        __thiscall B::B(void)
                                        __thiscall B::B(const struct B &)
                                        __thiscall B::B(const struct B &)
                                        __thiscall B::~B(void)
                                        __thiscall B::~B(void)
                                        __thiscall B::~B(void)
                                        
                                          +1
                                          Да, студия выжидает :)
                                            +2
                                            В моем случае она так довыжидалась, что я её на gcc 4.7 заменил!
                                        0
                                        Смотрите в этой таблице какие компиляторы поддерживают wiki.apache.org/stdcxx/C%2B%2B0xCompilerSupport

                                        Для того чтобы поддерживалась автоматическая генерация move конструкторов компилятор должен поддерживать R-Value References v3.0. На данный момент это только GCC version>=4.6 и возможно clang
                                  +1
                                  может ответ и не актуальный уже но в этом случае можно просто передавать по значению.

                                  class A
                                  {
                                  public:
                                      A(std::string name_) : name(std::move(name_)) {}
                                  private:
                                      std::string name;
                                  };
                                  
                                  0
                                  Правильно переводить не как «инстансирование», а как «инстанцирование».
                                    0
                                    Инстанс же.

                                    Хотя слух режет, конечно, это «инстансирование». Впрочем, как и «инстанцирование». Может быть лучше как-то иначе перевести.
                                      0
                                      Причем здесь режет или не режет.
                                      «Инстанцирование» — это устоявшийся термин, который широко применяется в литературе и тематических сайтах.
                                      0
                                      Спасибо, исправил.
                                      +1
                                      Ссылки являются более гибким понятием, чем lvalue ссылки или rvalue ссылки. Так rvalue ссылки могут быть связаны только с rvalue, а lvalue ссылки, в добавление к возможности привязки к lvalue, могут быть связаны с rvalue при ограниченных условиях (ограничения на связывание lvalue ссылок и rvalue заключается в том, что такое связывание допустимо только тогда, когда lvalue ссылка объявлена как ссылка на константу, т.е. const T&.) Ссылки же, объявленные с “&&”, которые могут быть либо lvalue ссылками, либо rvalue ссылками, могут быть связаны с чем угодно. Такие необычно гибкие ссылки заслуживают своего названия. Я назвал их «универсальными» ссылками.

                                      Я бы назвал это вин 80го уровня

                                      Only users with full accounts can post comments. Log in, please.