Почему `std::move` называется`std::move`?
C++11 std::move(x)
функция на самом деле ничего не двигает вообще. Это просто приведение к r-значению. Почему это было сделано? Разве это не вводит в заблуждение?
2 ответа
Правильно что std::move(x)
это просто приведение к rvalue - точнее к xvalue, а не prvalue. И это также верно, что имея бросок по имени move
иногда смущает людей. Однако цель этого именования не в том, чтобы запутать, а сделать ваш код более читабельным.
История move
восходит к первоначальному предложению о переезде в 2002 году. Эта статья сначала вводит ссылку на rvalue, а затем показывает, как написать более эффективный std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Следует напомнить, что на данный момент в истории, единственное, что "&&
"Возможно, это означало, что это было логично, и. Никто не был знаком ни с ссылками на rvalue, ни с последствиями приведения lvalue к rvalue (при этом не делая копию как static_cast<T>(t)
сделал бы). Поэтому читатели этого кода, естественно, подумают:
я знаю как
swap
должен работать (копировать во временные, а затем обмениваться значениями), но какова цель этих уродливых бросков?!
Обратите внимание, что swap
на самом деле просто замена для всех видов алгоритмов, изменяющих перестановку. Эта дискуссия намного, намного больше, чем swap
,
Затем предложение вводит синтаксис сахара, который заменяет static_cast<T&&>
с чем-то более читабельным, который передает не точное что, а скорее почему:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Т.е. move
это просто синтаксис сахара для static_cast<T&&>
и теперь код довольно наводит на мысль, почему эти приведения присутствуют: чтобы включить семантику перемещения!
Нужно понимать, что в контексте истории мало кто в этот момент действительно понимал тесную связь между значениями и семантикой перемещения (хотя в статье и это пытается объяснить):
Семантика перемещения автоматически вступает в игру, когда передается аргумент rvalue. Это совершенно безопасно, потому что остальная часть программы не может заметить перемещение ресурсов из значения r (никто не имеет ссылки на значение r, чтобы обнаружить разницу).
Если в то время swap
вместо этого было представлено так:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Тогда люди смотрели бы на это и говорили:
Но почему вы бросаете на Rvalue?
Основной момент:
Как это было, используя move
никто никогда не спрашивал:
Но почему ты двигаешься?
Шли годы, и предложение уточнялось, понятия lvalue и rvalue были уточнены до тех категорий ценностей, которые мы имеем сегодня:
(изображение беззастенчиво украдено из dirkgently)
И так сегодня, если бы мы хотели swap
чтобы точно сказать, что он делает, а не почему, он должен выглядеть так:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
И вопрос, который каждый должен задать себе, заключается в том, является ли приведенный выше код более или менее читабельным, чем:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Или даже оригинал:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
В любом случае, программист C++ подмастерье должен знать, что под капотом move
ничего более не происходит, чем актерский состав. И начинающий программист C++, по крайней мере, с move
, будут проинформированы, что намерение состоит в том, чтобы перейти от прав, а не копировать с прав, даже если они не понимают, как именно это достигается.
Кроме того, если программист желает эту функциональность под другим именем, std::move
не обладает монополией на эту функциональность, и в ее реализации не задействована непереносимая языковая магия. Например, если кто-то хотел закодировать set_value_category_to_xvalue
и используйте это вместо этого, это тривиально:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
В C++14 это становится еще более кратким:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Так что, если вы так склонны, украсьте свой static_cast<T&&>
как бы вы ни думали лучше, и, возможно, вы в конечном итоге разработаете новую лучшую практику (C++ постоянно развивается).
Так что же move
делать с точки зрения генерируемого объектного кода?
Учти этоtest
:
void
test(int& i, int& j)
{
i = j;
}
Составлено сclang++ -std=c++14 test.cpp -O3 -S
, это производит этот объектный код:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Теперь, если тест изменен на:
void
test(int& i, int& j)
{
i = std::move(j);
}
В объектном коде нет абсолютно никаких изменений. Можно обобщить этот результат для: Для тривиально подвижных объектов,std::move
не имеет никакого влияния
Теперь давайте посмотрим на этот пример:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Это создает:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
Если вы бежите __ZN1XaSERKS_
черезc++filt
он производит:X::operator=(X const&)
, Здесь нет ничего удивительного. Теперь, если тест изменен на:
void
test(X& i, X& j)
{
i = std::move(j);
}
Тогда в сгенерированном объектном коде по-прежнему нет никаких изменений. std::move
ничего не сделал, но бросил j
к значению, а затем это значениеX
привязывается к оператору присваивания копииX
,
Теперь давайте добавим оператор присваивания перемещенияX
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Теперь объектный код меняется:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Бег __ZN1XaSEOS_
через c++filt
показывает, что X::operator=(X&&)
вызывается вместо X::operator=(X const&)
,
И это все, что нужно std::move
! Он полностью исчезает во время выполнения. Его единственное влияние - во время компиляции, когда он может изменить то, что вызывается перегрузкой.
Позвольте мне оставить здесь цитату из часто задаваемых вопросов C++ 11, написанную Б. Страуструпом, которая является прямым ответом на вопрос ОП:
move (x) означает "вы можете рассматривать x как значение". Возможно, было бы лучше, если бы move () был вызван rval (), но к настоящему времени move () используется годами.
Кстати, мне очень понравился FAQ - его стоит прочитать.