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

129

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

Говард Хиннант
источник
Что еще хуже, три аргумента на std::moveсамом деле
сдвигаются
И не забывайте о C ++ 98/03/11 std::char_traits::move:-)
Говард Хиннант
30
Другой мой любимый вариант - это то, std::remove()что не удаляет элементы: вам все равно нужно вызвать, erase()чтобы удалить эти элементы из контейнера. Так moveчто не двигается, removeне удаляется. Я бы выбрал название mark_movable()для move.
Али
4
@Ali Я бы mark_movable()тоже запутался . Это предполагает наличие длительного побочного эффекта, которого на самом деле нет.
finnw

Ответы:

179

Верно , что 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 и семантикой перемещения (хотя в статье это также пытается объяснить):

Семантика перемещения автоматически вступает в игру при задании аргументов rvalue. Это совершенно безопасно, потому что перемещение ресурсов из rvalue не может быть замечено остальной частью программы ( никто другой не имеет ссылки на rvalue, чтобы обнаружить разницу ).

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

Говард Хиннант
источник
7
Вот точечный источник для этого графика: я воссоздал его digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }для всеобщего блага :) Загрузите его здесь как SVG
см.
7
Это все еще доступно для продажи велосипедов? Предлагаю allow_move;)
dyp
2
@dyp Мой любимый по-прежнему movable.
Daniel Frey
6
Скотт Мейерс предложил переименовать его std::moveв rvalue_cast: youtube.com/…
nairware
6
Поскольку теперь rvalue относится и к prvalues, и к xvalues, значение rvalue_castнеоднозначно: какое rvalue оно возвращает? xvalue_castбыло бы последовательным именем здесь. К сожалению, большинство людей в настоящее время также не понимают, что он делает. Надеюсь, что через несколько лет мое утверждение станет ложным.
Говард Хиннант
20

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

move (x) означает «вы можете рассматривать x как r-значение». Возможно, было бы лучше, если бы move () назывался rval (), но к настоящему времени move () используется уже много лет.

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

подкова
источник
2
Чтобы заимствовать комментарий @ HowardHinnant из другого ответа: ответ Страуструпа неточен, поскольку теперь есть два типа rvalue - prvalues ​​и xvalues, а std :: move действительно является приведением xvalue.
einpoklum