Чому `std :: move` названий` std :: move`?


128

Функція C ++ 11 std::move(x)насправді взагалі нічого не рухає. Це просто відступ на r-значення. Чому це було зроблено? Хіба це не вводить в оману?


Що ще гірше, три аргументи std::moveнасправді рухаються ..
Cubbi

І не забувайте про C ++ std::char_traits::move
98/03/11

30
Мій інший улюблений - це те, std::remove()що не видаляє елементи: вам все одно потрібно зателефонувати, erase()щоб фактично вилучити ці елементи з контейнера. Тому moveне рухається, removeне видаляється. Я б вибрав ім'я mark_movable()для move.
Алі

4
@Алі я також вважаю mark_movable()заплутаною. Це дозволяє припустити, що існує тривалий побічний ефект, де насправді його немає.
finnw

Відповіді:


177

Правильно, що std::move(x)це лише перелік оцінок - точніше до xvalue , на відміну від первого значення . І правда також, що те, що на ім'я акторської ролі moveіноді бентежить людей. Однак метою цього іменування є не плутати, а зробити ваш код більш читабельним.

Історія moveбере свій початок від початкової пропозиції щодо переходу в 2002 році . У цьому документі спочатку вводиться посилання на рецензію, а потім показано, як написати більш ефективне 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, щоб виявити різницю ).

Якщо в той час 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);
}

Тоді люди поглянули б на це і сказали:

Але чому ти кидаєш оцінювати?


Основний пункт:

Як це було, використовуючи move, ніхто ніколи не запитував:

Але чому ти рухаєшся?


Із плином років, і пропозиція вдосконалювалась, поняття lvalue та rvalue були перероблені на цінні категорії, які ми маємо сьогодні:

Таксономія

(образ безсоромно викрадений з безтурботного )

І тому сьогодні, якщо ми хотіли 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привласнення до 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
sehe

7
Це все-таки відкрито для велосипедних вилазок? Я б запропонував allow_move;)
dyp

2
@dyp Моя улюблена досі movable.
Даніель Фрей

6
Скотт Майєрс запропонував перейменувати його std::moveна rvalue_cast: youtube.com/…
nairware

6
Оскільки rvalue зараз посилається і на prvalues, і на xvalues, rvalue_castнеоднозначне за своїм значенням: який тип rvalue повертає? xvalue_castтут було б узгоджене ім'я. На жаль, більшість людей в цей час також не розуміли б, що це робить. Через кілька років, моє твердження, сподіваємось, стане помилковим.
Говард Хінант

20

Дозвольте лише залишити тут цитату із запитань C ++ 11, написаних Б. Струструпом, що є прямою відповіддю на питання ОП:

move (x) означає "ви можете ставитися до x як до рейтингу". Можливо, було б краще, якби переміщення () називалося rval (), але на сьогоднішній день move () використовується роками.

До речі, мені дуже сподобався FAQ - це варто прочитати.


2
Щоб плагіатувати коментар @ HowardHinnant з іншої відповіді: відповідь Stroustrup є неточною, оскільки зараз існує два типи rvalues ​​- prvalues ​​і xvalues, а std :: move - це дійсно величина xvalue.
einpoklum
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.