Комментарии 36
Зачем в примере с push_back(), который внутри вызывает emplace_back() используется std::move, ведь параметр _Val уже r-value?
А вот это довольно сложный момент в 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));
}
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. То есть, С++ в этом отношении ещё есть куда развиваться.
Хотя, ссылка типа
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
.
const T&
и
const T&&
в контексте «параметр функции»?
Внезапно, этот сценарий полностью покрывается ссылкой типа const T& v
.
Оказывается, не полностью…Почему числа не ведут себя, как другие объекты-значения?
struct string { };
void foo(string& s) { }
void bar(long long& v) { }
int main() {
// передаю временные объекты (rvalue) по ссылке
foo(string{}); // так нормально
bar(7LL); // а так не нормально!
return 0;
}
А куда это у вас const
потерялся?
https://stackoverflow.com/questions/36102728/why-is-it-allowed-to-pass-r-values-by-const-reference-but-not-by-normal-referenc
Я бы ещё понял, что константа лежит где-то в .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 не существовало.
Итак, наша foo выглядит теперь следующим образом:
Как это у вас после подстановки шаблона получилось foo(int p)
, а не foo(int&& p)
?
В том же абзаце чуть ранее: если в качестве аргумента передано 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.
Вообще идеальной была бы некая теория, исследующая эти вопросы на примере разных языков, с анализом достоинств и недостатков и попыткой построения некоего идеального псевдоязыка, решающего проблемы реально существующих реализаций.
Присоединяюсь, жесть. Зашёл освежить знания (когда-то раньше писал на C++/Qt) и практически пожалел - такая сложность на ровном месте! А реализацию на шаблонах лучше бы не открывал :/
В эти дни уже несколько языков превзошли C++ в некоторых его "коронных" областях применения, например Rust, Go, Julia. Превзойти во всех областях никто и не пытается вроде. А C++ так многолик, так много где используется, так обратно совместим - думаю именно в этом причина его сложности. Он как x86 в мире процессоров - дубовый, древний, тяжёлый, но надёжный и быстрый. Редизайн сделать можно, но высока вероятность, что получится как с Itanium :)
Редизайн сделать можно, но высока вероятность, что получится как с Itanium :)
В любом случае это было бы интересно.
Так место то как раз не ровное совсем. В языках с гц например, это место обходят через гц и не надо думать про разные типы ссылок. А в С++ это свойство подхода, и сложность складывается из двух частей (а) что можно управлять памятью на низком уровне (б) код может быть полиморфным по способу управления памяти. Вот это и приходится разруливать через std::forward
.
К вышеперечисленны еще можно добавить Zig в котором обобщенное программирование и compile-time execution сделаны совершенно иным образом и возможно даже лучше, при этом так же исповедуя zero-cost абстракции!
std::move vs. std::forward