Почему `std :: move` называется` std :: move`?

C ++ 11 std::move(x) функция на самом деле ничего не двигает вообще. Это просто приведение к r-значению. Почему это было сделано? Разве это не вводит в заблуждение?

116

Решение

Правильно что 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&&>и теперь код довольно наводит на мысль, почему эти приведения присутствуют: чтобы включить семантику перемещения!

Нужно понимать, что в контексте истории мало кто в этот момент действительно понимал тесную связь между значениями и семантикой перемещения (хотя в статье также это попыталось объяснить):

Семантика перемещения автоматически вступит в игру, когда получит значение
аргументы. Это совершенно безопасно, потому что перемещение ресурсов из
Значение не может быть замечено остальной частью программы (никто не имеет
ссылка на 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будут проинформированы о том, что переехать от RHS, в отличие от копия от rhs, даже если они точно не понимают как это достигнуто.

Кроме того, если программист желает эту функциональность под другим именем, 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! Он полностью исчезает во время выполнения. Единственное его влияние — во время компиляции, когда может быть изменить то, что называется перегрузка.

158

Другие решения

Позвольте мне оставить здесь цитату из C ++ 11 FAQ написанный Б. Страуструпом, который является прямым ответом на вопрос ОП:

move (x) означает «вы можете рассматривать x как значение». Может быть, это будет иметь
было бы лучше, если бы Move () был вызван rval (), но теперь Move () имеет
был использован в течение многих лет.

Кстати, мне очень понравился FAQ — его стоит прочитать.

19

По вопросам рекламы [email protected]