Pull to refresh

Comments 68

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

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

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

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

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

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

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

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

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

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() в нужном месте несколько позже)?

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.
Похоже, что && является признаком «разрушать можно»…

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

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

Этого места я не понял. Почему вызовется operator(const Widget& rhs), а не operator(Widget&& rhs)? Как тогда можно использовать lvalueness объекта var1, не прибегая к дополнительному std::move()?
Потому, что var1 есть lvalue типа Widget&&. Для lvalue всегда вызывается оператор копирования
А как мне добраться до его rvalue-содержимого?
Вы не понимаете. Нет никакого rvalue содержимого. Rvalue&rvalue есть лишь свойство выражения. Чтобы вызвать move семантику, нужно использовать rvalue выражение, для этого применяется, в частности, std::move(или как вы делали static_cast). Посмотрите мою статью. Может прояснит немного.
Посмотрел. И вообще перестал понимать разницу между T&& x=… и T x=… — в обоих случаях программе приходится выделять место на rvalue, и в обоих случаях она не может по своей воле использовать его как xvalue… Только в T&& x зачем-то требуется лишний указатель (и то не факт — адрес места под значение, на которое «указывает» x, она и так знает).
Разница в типе, выражение идентично.
Есть ли код, который после этой строчки будет вести себя по-разному (или в одном случае компилироваться, а в другом — нет)?
Разница лишь в том, что один объект ссылка, а второй нет. Со всеми вытекающими.
#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();
};
А оно будет работать? Это же я смогу написать

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

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

···

A&& x=boo();
x.foo();

Мой прогноз — напишет А. Если вообще скомпилируется.
Это UB, так как тут «повисшая» ссылка. Нельзя возвращать ссылку на локальный объект…
Это не локальный объект, это вычисленное выражение, rvalue. Его даже ни одной переменной не присвоили. А на что же еще может возвращать ссылку функция, описанная как A&& f();?
Это объект созданный в стеке функции. На lvalue, которое может быть уничтожено(так работает move). Нельзя просто так возвращать ссылку. Тут объект уже перестанет существовать, когда функция завершится.
Допустим, хотя мне это не очевидно (место для этого rvalue могло бы быть выделено вызывающей функцией — так, как это происходит при возврате структуры по значению). Но в таком случае вопрос остается: существует ли хотя бы одна корректная функция, описанная как A&& f()?
Можете написать. Внутри static A a; И возвращать rvlaue ссылку на него. Понятия не имею зачем, но это можно сделать. Тут те же правила, что и с lvalue ref. Только lvalue никто не «украдёт», когда его вернёшь.
Всё это очень странно. Получается, что заметная часть статьи (не относящаяся к вызову функций) посвящена конструкции, у которой мало того, что трудно найти практическое использование, так и просто нетривиального применения не видно.
Применение всегда найдется, а знать как всё работет очень полезно.
К примеру 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);
	}

в остальных компиляторах он будет такой же.
std::move — да, этот приём им нужен. В качестве лазейки из lvalue в rvalue. Кстати, как будет выглядеть её ассемблерный код, если написать её не inline (для конкретного типа) и запретить глобальную оптимизацию? Она просто вернет свой аргумент, или сделает что-нибудь менее тривиальное?
А что там может быть? Move семантика она выше уровнем чем ассемблер.
Действительно, больше ничего. То, что ссылка возвращается на ту же переменную, которую передали параметром, понятно… и далее вся цепочка раскручивается. :(
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);
}

Просто я действительно не знаю где применять предложенную Вами запись. Обычно rvalue применяется в качестве параметров, а не как объект создаваемый непосредственно в коде.
Вытащить ссылку из структуры для удобства?
type&& field = rvaluestruct.field;
Интересный вариант. Если мы напишем
F&& x=(A+B).field — сохранится ли A+B? Или нам скажут «низзя» потому что field — это lvalue?
может быть, T& «эквивалентен» все таки T *const, а не T const *?
Да, конечно. Нельзя менять переменную, а не *переменную. Я на С++ последние 10 лет пишу мало, и в тот момент не помнил, куда относится const.
Хорошая статья, спасибо.
Конечно, на первый взгляд все это действо может показаться весьма запутанным, особенно по части самих универсальных ссылок, но на практике все выглядит весьма удобно. В частности, контейнеры без rvalue семантики для меня уже немыслимы. Конечно, есть всякие стандартные и околостандартные трюки вроде RVO, std::swap специализации, но однако же это все лишь обходные средства.
UFO just landed and posted this here
С 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;
};

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

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

#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
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()

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

Кстати, 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)
Да, студия выжидает :)
В моем случае она так довыжидалась, что я её на gcc 4.7 заменил!
Смотрите в этой таблице какие компиляторы поддерживают wiki.apache.org/stdcxx/C%2B%2B0xCompilerSupport

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

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

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

Я бы назвал это вин 80го уровня
Sign up to leave a comment.

Articles