Как стать автором
Обновить

Комментарии 36

А вот это довольно сложный момент в C++.


Вообще, статья нисколько не объясняет, зачем же нам вообще этот std::forward нужен. Ведь если подумать, то он не делает ровным счётом ничего: на входе T& — на выходе T&, на входе T&& — на выходе T&&, а передача по значению у нас невозможна, ведь в аргументах функции — ссылки.


Наводящий момент:


void bar(int& v) { cout << "lvalue" << endl; }
void bar(int&& v) { cout << "rvalue" << endl; }
void foo(int&& v) { bar(v); }

Что вызовется? Ответ: bar(int& v). А почему? А вот так: несмотря на то, что переменная v имеет тип rvalue reference int&&, она относится к категории lvalue и потому передаётся дальше как lvalue reference int&.


И паттерн std::forward — это просто костыль, чтобы из lvalue сделать rvalue, но при этом не потеряв тип ссылки.

И паттерн std::forward — это просто костыль, чтобы из lvalue сделать rvalue, но при этом не потеряв тип ссылки.

Наверное, "std::move -- это просто [...]". forward как раз lvalue ref -> rvalue ref не делает. А вообще, здесь никаких костылей, просто реальность такова, что xvalue уничтожается только в конца блока, поэтому компилятор не может знать, когда будет последнее использование и куда воткнуть move.

НЛО прилетело и опубликовало эту надпись здесь

_Val является l-value, т.к. имеет имя и нее можно взять адрес в памяти. Поэтому ее нужно далее сделать r-value с помощью std::move. Пожалуй, надо дополнить статью этим моментом. Спасибо!

Это момент, который в обязательно порядке должен быть указан в статье.


Человеку, который знает, что такое lvalue, rvalue, lvalue reference, rvalue reference, и чем они отличаются друг от друга, статья безполезна, т.к. про move и forward знания у него тоже будут. А если человек не знает этой базы (на C++03, например, можно прекрасно программировать без неё), то и понять, затем нужны move/forward, ему будет достаточно проблематично.


То есть достаточно просто написать, что forward преобразует lvalue в rvalue с сохранением типа, объяснить затем это нужно (perfect forwarding), а всё остальное уже — просто детали реализации.


Ну и для дополнительного чтения (то же самое, но более подробно и понятно):
https://habr.com/ru/post/322132/

За совет спасибо! Я подумаю как тут лучше описать.

И кстати, forward преобразует lvalue в rvalue, только если передали аргумент как rvalue. Поэтому просто описать его работу одной и понятной фразой - сложная задача. Это move выполняет всегда cast к rvalue, а forward использует static_cast и сжатие ссылок. forward передает lvalue как l-value, а к rvalue применяет move, то есть приводит к rvalue;

И кстати, forward преобразует lvalue в rvalue, только если передали аргумент как rvalue.

Вот у вас, кстати, имеется непонимание этого момента.
Начнём с того, что rvalue и rvalue reference (&&) — это разные вещи. Первое — это категория значения, второе же относится к его типу.


Аргументы функции всегда являются lvalue, и не важно, передаются ли они по значению или по ссылке (& или && — не важно). То же самое касается и переменных: если вы сохраните результат функции в переменную, являющуюся rvalue reference (&&), то она будет lvalue.


Результат функции может быть как lvalue (возврат ссылки &), так и rvalue (возврат по значению или по ссылке &&).


Значениями, передаваемыми функции в качестве аргументов, являющихся rvalue reference, могут быть только rvalue. То есть следующий код не скомпилируется:


void bar(int &&arg) { }
void foo(int &&arg) { bar(arg); }

И только после осознания этих фактов наступит понимание, что именно делают std::move и std::forward:


std:move преобразует lvalue в rvalue и возвращает rvalue reference. Было T или T&& — стало T&&. Было T& — стало T&&. При этом, если значение, подаваемое на вход, уже является rvalue, то делать ему дополнительно std::move смысла нет:


void bar(T &&arg) {}
T meow() { return ... }
void foo() { bar(meow()); }

Здесь результат meow() не является rvalue reference, но является rvalue, и потом может быть передан в качестве аргумента для bar.


С std::forward немного посложнее. Сам по себе этот шаблон бессмысленен и применим только для узкого сценария: perfect forwarding для шаблонных аргументов, передаваемых в виде универсальной ссылки. Как и в случае std::move, он преобразовывает lvalue в rvalue, но только в случае, если тип аргумента не является lvalue reference.

Нет, внутри push_back(T&&) аргумент имеет имя _Val и является lvalue, которое нужно двигать дальше в emplace_back(), безусловно превращая _Val в rvalue с помощью std::move().

В примере с make_unique(Types&& ... _Args) параметры `_Args` могут быть как lvalue, так и rvalue, значит применяем std::forward<Types>(_Args)..., которая на этапе компиляции разберется, где l- и где r-value.

Неформально я запомнил это для себя так: если шаблонный параметр надо передать куда-то дальше, то используем для передачи std::forward():

template<typename T> func(T&& arg)

{

// SomeClass has a constructor for T

std::vector<SomeClass> vector;

vector.emplace_back(std::forward<T>(arg));

}

Для не шаблонной rvalue ссылки используем, если надо, std::move():

void func(std::string&& str)

{

std::vector<std::string> vector;

vector.push_back(std::move(str));

}

Спасибо вам за подробное объяснение и всем другим, ответившим на мой вопрос. Теперь всё понятно.

Как я это понимаю: любой именованый объект становится lvalue. Даже если это аргумент функции void f(T&& v), когда вы внутри функции будете обращаться к v — в контексте этой функции у него уже есть имя и это уже не rvalue, хоть он и был объявлен как rvalue reference (&&). Это нужно, например, потому, что вы можете захотеть вызвать с ним несколько функций:

void f(T&& v) {
   process(v);
   process_some_more(v);
   process_yet_more(v);
}


Если бы символ v имел категорию rvalue, а функция process имела бы перегрузку для rvalue, то она могла бы «сломать» этот объект (переместив его содержимое в другое место, для чего, собственно, мы и хотим делать такие перегрузки — для оптимизации). Поэтому v — это lvalue, а чтобы передать этот объект дальше именно как rvalue, нужно опять «потерять» его имя, для чего и нужен std::move.
Для меня эта новая мысль — forwarding reference как индикатор, что ф-ция _может_ сломать объект.

К сожалению, временный объект можно передавать по такой ссылке не только с целью перемещения. Хотелось бы, чтобы синтаксически было понятно, с какой целью в ф-ции используется forwarding reference. То есть, С++ в этом отношении ещё есть куда развиваться.

Хотя, ссылка типа const T&& v явно говорит, что ломать объект v мы не собираемся, а && тут для того, чтобы можно было пользоваться временными объектами без передачи по значению.

Но иногда бывает что-то типа
T&& operator << (T&& v, const std::string& str) {
    v.append(str);
    return std::move(v);
}

Объект не ломаем, но и const нет.
Хотя, ссылка типа const T&& v явно говорит, что ломать объект v мы не собираемся, а && тут для того, чтобы можно было пользоваться временными объектами без передачи по значению.

Внезапно, этот сценарий полностью покрывается ссылкой типа const T& v.

Разница в том, что по первой ссылке вы можете передавать и lvalue, и rvalue, а по второй — только rvalue. Но а так как вы объект трогать не собираетесь (на что указывает модификатор const), то использование второй ссылки бессмысленно.

Внезапно, этот сценарий полностью покрывается ссылкой типа const T& v.
Оказывается, не полностью…

Почему числа не ведут себя, как другие объекты-значения?

struct string { };

void foo(string& s) { }

void bar(long long& v) { }

int main() {
    // передаю временные объекты (rvalue) по ссылке
    foo(string{}); // так нормально
    bar(7LL); // а так не нормально!
    return 0;
}
На численное rvalue, внезапно, можно взять const-ссылку.
Я бы ещё понял, что константа лежит где-то в .rdata и компилятор передаёт ссылку на неё. Но ведь и на выражения типа (x+3) можно взять ссылку const int&, но нельзя int&
Загадка…

К слову, компилятор ругается на обе ваши строчки:


5.cpp: In function ‘int main()’:
5.cpp:10:9: error: cannot bind non-const lvalue reference of type ‘string&’ to an rvalue of type ‘string’
   10 |     foo(string{}); // так нормально
      |         ^~~~~~~~
5.cpp:4:18: note:   initializing argument 1 of ‘void foo(string&)’
    4 | void foo(string& s) { }
      |          ~~~~~~~~^
5.cpp:11:9: error: cannot bind non-const lvalue reference of type ‘long long int&’ to an rvalue of type ‘long long int’
   11 |     bar(7LL); // а так не нормально!
      |         ^~~
5.cpp:6:21: note:   initializing argument 1 of ‘void bar(long long int&)’
    6 | void bar(long long& v) { }
      |          ~~~~~~~~~~~^

И нет, это не загадка. Просто так описано в стандарте.
Читайте про Dangling References:
https://en.cppreference.com/w/cpp/language/reference
Там написано:
Note that rvalue references and lvalue references to const extend the lifetimes of temporary objects


Отсюда следует, что rvalue может быть передано дальше только как const T& или T&&.


А причину именно такой логики стоит, видимо, искать в старых стандартах, когда rvalue reference не существовало.

MSVC не ругается на foo(string{});

У Microsoft всегда было своё восприятие стандарта.

В том же абзаце чуть ранее: если в качестве аргумента передано rvalue, то T выводится как бессылочный тип. Именно этот фокус и обеспечивает правильную работу forward.

Привожу кусок вашего же кода:


template<typename T>
void foo(T&& p)
{
    bar(p);
    bar(_move(p));
    bar(_forward<T>(p));
}

int main()
{
    int i = 0;
    foo(i); // lvalue: T - int&, p - int&
    foo(0); // rvalue: T - int, p - int&&
}

Разберем теперь rvalue. В функции foo тип T будет выведен как int. Согласно все тех же правил вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано rvalue, то T выводится как бессылочный тип.

Ну и как же foo(T&& p) при T = int превратилась в foo(int p), а не в foo(int&& p)?

Теперь понял. Это косяк, конечно. Обязательно поправлю. Спасибо большое!

На мой взгляд комитет выбрал не самое удачное имя для std::move(), которое только вносит путаницу.

Согласен. Где-то читал, что было предложение использовать то ли rvalue_cast, то ли cast_rvalue. Не суть уже. Универсальные ссылки тоже не устроили комитет. На cppcon2014 приняли решение именовать их как forwarding references.

rvalue_cast путалась бы с ключевыми словами языка, как static_cast, const_cast…
Вообще конечно жуть. Не могу отделаться от мысли, что все это как-то неправильно, криво и костыльно. Я вот думаю — а если бы проектировали язык уровня С++ с нуля, то можно было бы спроектировать иначе? Более понятно и прозрачно?
rust :)
Это слишком общий ответ) Хотелось бы конкретики.
Вообще идеальной была бы некая теория, исследующая эти вопросы на примере разных языков, с анализом достоинств и недостатков и попыткой построения некоего идеального псевдоязыка, решающего проблемы реально существующих реализаций.

Все равно rust. Причем он такой один.

Присоединяюсь, жесть. Зашёл освежить знания (когда-то раньше писал на C++/Qt) и практически пожалел - такая сложность на ровном месте! А реализацию на шаблонах лучше бы не открывал :/

В эти дни уже несколько языков превзошли C++ в некоторых его "коронных" областях применения, например Rust, Go, Julia. Превзойти во всех областях никто и не пытается вроде. А C++ так многолик, так много где используется, так обратно совместим - думаю именно в этом причина его сложности. Он как x86 в мире процессоров - дубовый, древний, тяжёлый, но надёжный и быстрый. Редизайн сделать можно, но высока вероятность, что получится как с Itanium :)

Редизайн сделать можно, но высока вероятность, что получится как с Itanium :)

В любом случае это было бы интересно.

Так место то как раз не ровное совсем. В языках с гц например, это место обходят через гц и не надо думать про разные типы ссылок. А в С++ это свойство подхода, и сложность складывается из двух частей (а) что можно управлять памятью на низком уровне (б) код может быть полиморфным по способу управления памяти. Вот это и приходится разруливать через std::forward.

К вышеперечисленны еще можно добавить Zig в котором обобщенное программирование и compile-time execution сделаны совершенно иным образом и возможно даже лучше, при этом так же исповедуя zero-cost абстракции!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории