Что такое семантика перемещения?

1706

Я только что закончил слушать радио- интервью подкаста Software Engineering со Скоттом Мейерсом о C ++ 0x . Большинство новых функций имело смысл для меня, и сейчас я на самом деле рад C ++ 0x, за исключением одного. Я до сих пор не понимаю семантику перемещения ... Что это такое?

dicroce
источник
20
Я нашел [статью Эли Бендерского в блоге] ( eli.thegreenplace.net/2011/12/15/… ) о lvalues ​​и rvalues ​​в C и C ++ довольно информативным. Он также упоминает ссылки на rvalue в C ++ 11 и представляет их с небольшими примерами.
Нильс
16
Экспозиция Алекса Аллена на эту тему очень хорошо написана.
Патрик Санан
19
Каждый год или около того я задаюсь вопросом, что такое «новая» семантика перемещения в C ++, я гуглю ее и попадаю на эту страницу. Я читаю ответы, мой мозг отключается. Я возвращаюсь к С и все забываю! Я зашел в тупик.
небо
7
@sky Рассмотрим std :: vector <> ... Где-то там есть указатель на массив в куче. Если вы копируете этот объект, новый буфер должен быть выделен, и данные из буфера должны быть скопированы в новый буфер. Есть ли обстоятельства, при которых можно было бы просто украсть указатель? Ответ ДА, когда компилятор знает, что объект является временным. Семантика перемещения позволяет вам определить, как кишки ваших классов могут быть перемещены и удалены в другом объекте, когда компилятор знает, что объект, из которого вы перемещаетесь, собирается исчезнуть.
18:00
Единственная ссылка, которую я могу понять: learncpp.com/cpp-tutorial/… , т. Е. Исходное обоснование семантики перемещения - это умные указатели.
19

Ответы:

2484

Мне проще понять семантику перемещения с помощью примера кода. Давайте начнем с очень простого строкового класса, который содержит только указатель на выделенный в куче блок памяти:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Поскольку мы решили сами управлять памятью, нам нужно следовать правилу трех . Я собираюсь отложить написание оператора присваивания и пока реализовать только деструктор и конструктор копирования:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Конструктор копирования определяет, что значит копировать строковые объекты. Параметр const string& thatпривязывается ко всем выражениям типа string, что позволяет вам делать копии в следующих примерах:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Теперь приходит ключ к пониманию семантики перемещения. Обратите внимание, что только в первой строке, где мы копируем x, эта глубокая копия действительно необходима, потому что мы могли бы захотеть проверить xпозже и были бы очень удивлены, если бы xчто-то изменилось. Вы заметили, как я только что сказал xтри раза (четыре раза, если вы включите это предложение) и имел в виду один и тот же объект каждый раз? Мы называем такие выражения, как x«lvalues».

Аргументы в строках 2 и 3 - это не lvalues, а rvalues, потому что нижележащие строковые объекты не имеют имен, поэтому у клиента нет возможности проверить их снова в более поздний момент времени. Значения r обозначают временные объекты, которые уничтожаются в следующей точке с запятой (точнее: в конце полного выражения, которое лексически содержит значение r). Это важно, потому что во время инициализацииb и cмы могли делать все, что хотели с исходной строкой, и клиент не мог сказать разницу !

C ++ 0x представляет новый механизм, называемый «ссылкой на rvalue», который, помимо прочего, позволяет обнаруживать аргументы rvalue через перегрузку функций. Все, что нам нужно сделать, это написать конструктор со ссылочным параметром rvalue. Внутри этого конструктора мы можем делать с источником все, что захотим , при условии, что мы оставляем его в каком-то допустимом состоянии:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Что мы здесь сделали? Вместо глубокого копирования данных кучи, мы просто скопировали указатель и затем установили исходный указатель на ноль (чтобы не допустить «delete []» из деструктора исходного объекта для освобождения наших «только что украденных данных»). По сути, мы «украли» данные, которые изначально принадлежали исходной строке. Опять же, ключевой момент заключается в том, что ни при каких обстоятельствах клиент не может обнаружить, что источник был изменен. Поскольку мы не делаем здесь копию, мы называем этот конструктор «конструктором перемещения». Его задача - перемещать ресурсы с одного объекта на другой, а не копировать их.

Поздравляем, теперь вы понимаете основы семантики перемещения! Давайте продолжим, реализовав оператор присваивания. Если вы не знакомы с идиомой копирования и обмена , изучите ее и возвращайтесь, потому что это потрясающая идиома C ++, связанная с безопасностью исключений.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Да это все? "Где ссылка? Вы можете спросить. "Нам здесь не нужно!" мой ответ :)

Обратите внимание, что мы передаем параметр that по значению , поэтому thatего нужно инициализировать, как и любой другой строковый объект. Как именно thatбудет инициализироваться? В былые времена C ++ 98 ответом был бы «конструктор копирования». В C ++ 0x компилятор выбирает между конструктором копирования и конструктором перемещения в зависимости от того, является ли аргумент оператора присваивания lvalue или rvalue.

Поэтому, если вы скажете a = b, конструктор копирования будет инициализирован that(потому что выражение bявляется lvalue), а оператор присваивания заменяет содержимое только что созданной глубокой копией. Это само определение копии и идиома замены - создайте копию, обменяйте содержимое копией, а затем избавьтесь от копии, покинув область действия. Здесь нет ничего нового.

Но если вы скажете a = x + y, конструктор перемещения будет инициализирован that(потому что выражение x + yявляется r-значением), так что здесь не требуется глубокая копия, а только эффективное перемещение. thatвсе еще является независимым объектом от аргумента, но его построение было тривиальным, так как данные кучи не нужно было копировать, просто перемещать. Не было необходимости копировать его, потому что оно x + yявляется rvalue, и, опять же, можно перейти от строковых объектов, обозначенных rvalue.

Подводя итог, конструктор копирования делает глубокую копию, потому что источник должен оставаться нетронутым. С другой стороны, конструктор перемещения может просто скопировать указатель, а затем установить нулевой указатель в источнике. Можно «обнулить» исходный объект таким образом, потому что у клиента нет способа снова проверить объект.

Я надеюсь, что этот пример объяснил главное. Существует намного больше, чтобы ценить ссылки и перемещать семантику, которую я намеренно оставил для простоты. Если вы хотите получить более подробную информацию, пожалуйста, смотрите мой дополнительный ответ .

fredoverflow
источник
40
@ Но если мой ctor получает rvalue, который никогда не может быть использован позже, почему мне даже нужно беспокоиться о том, чтобы оставить его в согласованном / безопасном состоянии? Вместо того, чтобы устанавливать that.data = 0, почему бы просто не оставить это?
einpoklum
70
@einpoklum Потому что без that.data = 0, персонажи будут уничтожены слишком рано (когда умрет временный), а также дважды. Вы хотите украсть данные, а не делиться ими!
fredoverflow
19
@einpoklum Запланированный деструктор все еще запускается, поэтому вы должны убедиться, что состояние после перемещения исходного объекта не вызывает сбой. Лучше убедиться, что исходный объект также может быть получателем назначения или другой записи.
CTMacUser
12
@pranitkothari Да, все объекты должны быть уничтожены, даже удалены от объектов. И так как мы не хотим, чтобы массив char удалялся, когда это происходит, мы должны установить указатель в null.
fredoverflow
7
@ Virus721 для nullptr delete[]определяется стандартом C ++ как неиспользуемый .
fredoverflow
1059

Моим первым ответом было предельно упрощенное введение в перемещение семантики, и многие детали были упущены с целью упростить его. Тем не менее, есть еще много чего изменить семантику, и я подумал, что пришло время для второго ответа, чтобы заполнить пробелы. Первый ответ уже довольно старый, и было бы неправильно просто заменить его совершенно другим текстом. Я думаю, что это все еще служит хорошим введением. Но если вы хотите копать глубже, читайте дальше :)

Стефан Т. Лававей нашел время, чтобы дать ценные отзывы. Большое спасибо, Стефан!

Введение

Семантика перемещения позволяет объекту при определенных условиях вступать во владение внешними ресурсами какого-либо другого объекта. Это важно двумя способами:

  1. Превращение дорогих копий в дешевые ходы. Смотрите мой первый ответ для примера. Обратите внимание, что если объект не управляет хотя бы одним внешним ресурсом (напрямую или косвенно через свои объекты-члены), семантика перемещения не даст никаких преимуществ по сравнению с семантикой копирования. В этом случае копирование объекта и перемещение объекта означают одно и то же:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. Реализация безопасных типов «только для перемещения»; то есть типы, для которых копирование не имеет смысла, но перемещение имеет смысл. Примеры включают в себя блокировки, файловые дескрипторы и интеллектуальные указатели с уникальной семантикой владения. Примечание. В этом ответе обсуждается std::auto_ptrустаревший шаблон стандартной библиотеки C ++ 98, который был заменен на std::unique_ptrC ++ 11. Программисты среднего уровня C ++, вероятно, хотя бы немного знакомы с ним std::auto_ptr, и из-за отображаемой им «семантики перемещения» это кажется хорошей отправной точкой для обсуждения семантики перемещения в C ++ 11. YMMV.

Что такое ход?

Стандартная библиотека C ++ 98 предлагает интеллектуальный указатель с уникальной семантикой владения std::auto_ptr<T>. В случае, если вы не знакомы auto_ptr, его цель - гарантировать, что динамически размещаемый объект всегда освобождается, даже при исключениях:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Необычная вещь о auto_ptrего "копирующем" поведении:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Обратите внимание , как инициализация bс aвовсе не копировать треугольник, но вместо этого передает право собственности на треугольнике от aдо b. Мы также говорят , что « aбудет перемещен в b » или «треугольник перемещается из a к b ». Это может показаться странным, потому что сам треугольник всегда остается в памяти в одном месте.

Переместить объект означает передать право собственности на некоторый ресурс, которым он управляет, на другой объект.

Конструктор копирования, auto_ptrвероятно, выглядит примерно так (несколько упрощенно):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Опасные и безобидные ходы

Опасность в том, auto_ptrчто то, что синтаксически выглядит как копия, на самом деле является движением. Попытка вызова функции-члена в Move-from auto_ptrвызовет неопределенное поведение, поэтому вы должны быть очень осторожны, чтобы не использовать функцию auto_ptrпосле ее перемещения из:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Но auto_ptrэто не всегда опасно. Заводские функции - прекрасный вариант использования для auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Обратите внимание, что оба примера следуют одному и тому же синтаксическому шаблону:

auto_ptr<Shape> variable(expression);
double area = expression->area();

И все же один из них вызывает неопределенное поведение, тогда как другой - нет. Так в чем же разница между выражениями aи make_triangle()? Разве они не одного типа? На самом деле они есть, но у них есть разные категории стоимости .

Категории значений

Очевидно, что между выражением, aкоторое обозначает auto_ptrпеременную, и выражением, make_triangle()которое обозначает вызов функции, возвращающей auto_ptrзначение by , должно быть какое-то глубокое различие , создавая тем самым свежий временный auto_ptrобъект каждый раз, когда он вызывается. aявляется примером lvalue , тогда как make_triangle()является примером rvalue .

Переход от значений l, таких как aопасный, потому что позже мы можем попытаться вызвать функцию-член a, вызывая неопределенное поведение. С другой стороны, переход от значений r, таких как make_triangle()совершенно безопасный, потому что после того, как конструктор копирования выполнил свою работу, мы не можем снова использовать временные. Нет выражения, которое обозначает временное; если мы просто напишем make_triangle()снова, мы получим другой временный. Фактически, перемещенный из временного уже ушел на следующую строку:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Обратите внимание, что буквы lи rимеют историческое происхождение в левой и правой части задания. Это больше не верно в C ++, потому что есть l-значения, которые не могут появляться в левой части присваивания (например, массивы или пользовательские типы без оператора присваивания), и есть r-значения, которые могут (все r-значения типов классов). с оператором присваивания).

Значение класса - это выражение, оценка которого создает временный объект. При нормальных обстоятельствах никакое другое выражение в той же области не обозначает тот же временный объект.

Rvalue ссылки

Теперь мы понимаем, что переход от lvalues ​​потенциально опасен, но переход от rvalues ​​безвреден. Если бы в C ++ была языковая поддержка, чтобы отличать аргументы lvalue от аргументов rvalue, мы могли бы либо полностью запретить переход от lvalue, либо, по крайней мере, сделать переход от lvalue явным на сайте вызова, чтобы мы больше не перемещались случайно.

Ответ C ++ 11 на эту проблему - rvalue ссылки . Ссылка на rvalue - это новый вид ссылок, который привязывается только к rvalue, а синтаксис - это X&&. Старая добрая ссылка X&теперь называется ссылкой lvalue . (Обратите внимание, что X&&это не ссылка на ссылку; такого нет в C ++.)

Если мы добавим constв микс, у нас уже есть четыре разных типа ссылок. С какими типами выражений Xони могут связываться?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

На практике вы можете забыть о const X&&. Ограничение чтения из значений не очень полезно.

Ссылка на rvalue X&&- это новый вид ссылок, который привязывается только к rvalue.

Неявные преобразования

Rvalue ссылки прошли через несколько версий. Начиная с версии 2.1, ссылка rvalue X&&также связывается со всеми категориями значений другого типа Y, при условии, что существует неявное преобразование из Yв X. В этом случае создается временный тип X, и ссылка на rvalue привязывается к этому временному:

void some_function(std::string&& r);

some_function("hello world");

В приведенном выше примере "hello world"это lvalue типа const char[12]. Поскольку существует неявное преобразование из const char[12]сквозного const char*в std::string, создается временный тип std::stringи rпривязывается к этому временному объекту. Это один из случаев, когда различие между значениями (выражениями) и временными значениями (объектами) немного размыто.

Переместить конструкторы

Полезный пример функции с X&&параметром - конструктор перемещения X::X(X&& source) . Его целью является передача права собственности на управляемый ресурс из источника в текущий объект.

В C ++ 11 std::auto_ptr<T>был заменен на std::unique_ptr<T>который использует ссылки на rvalue. Я буду разрабатывать и обсуждать упрощенную версию unique_ptr. Во- первых, мы инкапсулировать сырой указатель и перегружать операторы ->и *, поэтому наш класс чувствует , как указатель:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Конструктор становится владельцем объекта, а деструктор удаляет его:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Теперь перейдем к интересной части, конструктору перемещения:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Этот конструктор перемещения делает именно то, что auto_ptrсделал конструктор копирования, но он может быть предоставлен только с rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

Вторая строка не компилируется, потому что aэто lvalue, но параметр unique_ptr&& sourceможет быть привязан только к rvalue. Это именно то, что мы хотели; опасные действия никогда не должны быть скрытыми. Третья строка компилируется просто отлично, потому что make_triangle()это значение. Конструктор перемещения переведет владение из временного объекта в c. Опять же, это именно то, что мы хотели.

Конструктор перемещения передает владение управляемым ресурсом текущему объекту.

Операторы назначения перемещения

Последним недостающим элементом является оператор присваивания перемещения. Его задача - освободить старый ресурс и получить новый ресурс из его аргумента:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Обратите внимание, что эта реализация оператора присваивания перемещения дублирует логику как деструктора, так и конструктора перемещения. Вы знакомы с идиомой копирования и обмена? Он также может быть применен для перемещения семантики как идиома перемещения и обмена:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Теперь sourceэто переменная типа unique_ptr, она будет инициализирована конструктором перемещения; то есть аргумент будет перемещен в параметр. Аргумент все еще должен быть rvalue, потому что сам конструктор перемещения имеет ссылочный параметр rvalue. Когда поток управления достигает закрывающей скобки operator=, sourceвыходит из области видимости, автоматически освобождая старый ресурс.

Оператор назначения перемещения передает владение управляемым ресурсом текущему объекту, освобождая старый ресурс. Идиома перемещения и обмена упрощает реализацию.

Переезд из lvalues

Иногда мы хотим отойти от lvalues. То есть иногда мы хотим, чтобы компилятор обрабатывал lvalue, как если бы он был rvalue, чтобы он мог вызывать конструктор move, даже если он потенциально может быть небезопасным. Для этой цели C ++ 11 предлагает стандартный шаблон библиотечной функции, который вызывается std::moveвнутри заголовка <utility>. Это имя немного прискорбно, потому что std::moveпросто приводит lvalue к rvalue; он ничего не двигает сам по себе. Это просто позволяет двигаться. Возможно это должно было быть названо std::cast_to_rvalueили std::enable_move, но мы застряли с именем к настоящему времени.

Вот как вы явно переходите от lvalue:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Обратите внимание, что после третьей строки aбольше не принадлежит треугольник. Это нормально, потому что, явно написав std::move(a), мы четко заявили о наших намерениях: «Дорогой конструктор, делай все, что хочешь a, чтобы инициализировать c; мне уже все равно a. Не стесняйся иметь свой путь a».

std::move(some_lvalue) преобразует lvalue в rvalue, что дает возможность последующего перемещения.

Xvalues

Обратите внимание, что хотя std::move(a)это и является значением, его оценка не создает временный объект. Эта загадка вынудила комитет ввести третью категорию стоимости. То, что может быть связано с ссылкой на rvalue, даже если оно не является rvalue в традиционном смысле, называется xvalue (значение eXpiring). Традиционные значения были переименованы в prvalues (чистые значения).

И prvalues, и xvalues ​​являются rvalues. Значения xvalue и lvalue являются glvalues (Обобщенные lvalues). Отношения легче понять с помощью диаграммы:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Обратите внимание, что только значения xval действительно новые; остальное только за счет переименования и группировки.

Значения в C ++ 98 известны как значения в C ++ 11. Мысленно замените все вхождения «rvalue» в предыдущих абзацах на «prvalue».

Выход из функций

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

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Возможно, что удивительно, автоматические объекты (локальные переменные, которые не объявлены как static) также могут быть неявно удалены из функций:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Почему конструктор перемещения принимает значение lvalue resultв качестве аргумента? Область действия resultблизится к концу, и она будет уничтожена при разматывании стека. Никто не мог потом жаловаться, что resultкак-то изменилось; когда поток управления возвращается у вызывающего, resultбольше не существует! По этой причине в C ++ 11 есть специальное правило, которое позволяет автоматически возвращать объекты из функций без необходимости писать std::move. Фактически, вы никогда не должны использовать std::moveдля перемещения автоматических объектов из функций, так как это запрещает «оптимизацию именованных возвращаемых значений» (NRVO).

Никогда не используйте std::moveдля перемещения автоматических объектов из функций.

Обратите внимание, что в обеих фабричных функциях тип возвращаемого значения - это значение, а не ссылка на значение. Rvalue-ссылки по-прежнему являются ссылками, и, как всегда, вы никогда не должны возвращать ссылку на автоматический объект; вызывающая сторона получит висячую ссылку, если вы обманом заставите компилятор принять ваш код, например так:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Никогда не возвращайте автоматические объекты по ссылке. Перемещение выполняется исключительно конструктором перемещения std::move, а не просто связыванием rvalue со ссылкой на rvalue.

Переезд в члены

Рано или поздно вы напишите такой код:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

По сути, компилятор будет жаловаться, что parameterэто lvalue. Если вы посмотрите на его тип, вы увидите ссылку rvalue, но ссылка rvalue просто означает «ссылку, связанную с rvalue»; это не значит, что сама ссылка является ценным! Действительно, parameterэто просто обычная переменная с именем. Вы можете использовать parameterстолько раз, сколько захотите, внутри тела конструктора, и он всегда обозначает один и тот же объект. Неявное движение от него было бы опасно, поэтому язык запрещает это.

Именованная ссылка на rvalue является lvalue, как и любая другая переменная.

Решение состоит в том, чтобы вручную включить перемещение:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Можно утверждать, что parameterбольше не используется после инициализации member. Почему не существует специального правила для тихой вставки, std::moveкак с возвращаемыми значениями? Возможно, потому что это будет слишком большой нагрузкой для разработчиков компилятора. Например, что, если тело конструктора было в другом модуле перевода? Напротив, правило возвращаемого значения просто должно проверять таблицы символов, чтобы определить, returnобозначает ли идентификатор после ключевого слова автоматический объект.

Вы также можете передать parameterпо значению. Для типов типа «только для перемещения» unique_ptr, похоже, еще не существует идиомы. Лично я предпочитаю передавать по значению, так как это вызывает меньше помех в интерфейсе.

Специальные функции-члены

C ++ 98 неявно объявляет три специальные функции-члены по требованию, то есть когда они где-то нужны: конструктор копирования, оператор присваивания копии и деструктор.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Rvalue ссылки прошли через несколько версий. Начиная с версии 3.0, C ++ 11 объявляет две дополнительные специальные функции-члены по требованию: конструктор перемещения и оператор присваивания перемещения. Обратите внимание, что ни VC10, ни VC11 пока не соответствуют версии 3.0, поэтому вам придется реализовать их самостоятельно.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Эти две новые специальные функции-члены объявляются неявно, только если ни одна из специальных функций-членов не объявляется вручную. Кроме того, если вы объявляете свой собственный конструктор перемещения или оператор присваивания перемещения, ни конструктор копирования, ни оператор присваивания копии не будут объявлены неявно.

Что эти правила означают на практике?

Если вы пишете класс без неуправляемых ресурсов, нет необходимости объявлять какую-либо из пяти специальных функций-членов самостоятельно, и вы получите правильную семантику копирования и бесплатно переместите семантику. В противном случае вам придется самостоятельно реализовать специальные функции-члены. Конечно, если ваш класс не извлекает выгоду из семантики перемещения, нет необходимости реализовывать специальные операции перемещения.

Обратите внимание, что оператор присваивания копии и оператор присваивания перемещения могут быть объединены в единый унифицированный оператор присваивания, принимая его аргумент по значению:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

Таким образом, количество специальных функций-членов для реализации уменьшается с пяти до четырех. Здесь есть компромисс между безопасностью исключений и эффективностью, но я не эксперт в этом вопросе.

Пересылка ссылок ( ранее называемых универсальными ссылками )

Рассмотрим следующий шаблон функции:

template<typename T>
void foo(T&&);

Вы можете ожидать T&&привязки только к rvalue, потому что на первый взгляд это похоже на ссылку на rvalue. Как выясняется, однако, T&&также привязывается к lvalues:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Если аргумент является значением типа X , Tвыводится как X, следовательно, T&&означает X&&. Это то, что можно было ожидать. Но если аргумент является lvalue типа X, из-за специального правила, Tвыводится X&, следовательно, T&&будет означать что-то вроде X& &&. Но так как C ++ до сих пор понятия не имеет ссылок на ссылки, тип X& &&будет разрушилась в X&. Поначалу это может показаться запутанным и бесполезным, но свертывание ссылок важно для идеальной пересылки (что здесь не обсуждается).

T && - это не ссылка на значение, а ссылка на пересылку. Он также связывается с lvalues, в каком случае Tи T&&оба Lvalue ссылки.

Если вы хотите ограничить шаблон функции значениями r, вы можете объединить SFINAE с чертами типа:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Осуществление переезда

Теперь, когда вы понимаете сворачивание ссылок, вот как std::moveэто реализовано:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Как вы видете, move принимает любой вид параметра благодаря ссылке на пересылку T&&и возвращает ссылку на значение. std::remove_reference<T>::typeВызов мета-функция необходима , так как в противном случае, для lvalues типа X, тип возвращаемого бы X& &&, что бы рухнуть в X&. Так как tэто всегда lvalue (помните, что именованная ссылка rvalue является lvalue), но мы хотим привязать tссылку rvalue, мы должны явно привести tк правильному возвращаемому типу. Вызов функции, которая возвращает ссылку на rvalue, сам по себе является xvalue. Теперь вы знаете, откуда взялись xvalues;)

Вызов функции, которая возвращает ссылку на rvalue, например std::move, является значением xvalue.

Обратите внимание, что возвращение по ссылке rvalue в этом примере хорошо, потому tчто не обозначает автоматический объект, но вместо этого объект, который был передан вызывающей стороной.

fredoverflow
источник
24
Существует третья причина, по которой важна семантика перемещения: безопасность исключений. Часто, когда операция копирования может генерировать (потому что ей нужно распределить ресурсы, а выделение может произойти сбой), операция перемещения может быть безудержной (потому что она может передавать владение существующим ресурсам вместо выделения новых). Операции, которые не могут завершиться неудачей, всегда хороши, и это может иметь решающее значение при написании кода, который обеспечивает гарантии исключений.
Брангдон
8
Я был с вами вплоть до «Универсальных ссылок», но тогда это слишком абстрактно, чтобы следовать. Ссылка рушится? Идеальная пересылка? Вы говорите, что ссылка на rvalue становится универсальной ссылкой, если тип является шаблонным? Хотелось бы, чтобы был способ объяснить это, чтобы я знал, нужно ли мне это понимать или нет! :)
Kylotan
8
Пожалуйста, напишите книгу сейчас ... этот ответ дал мне основание полагать, что если вы осветите другие аспекты C ++ таким ясным образом, тысячи других людей поймут это.
Халивингстон,
12
@halivingston Большое спасибо за ваши добрые отзывы, я очень ценю это. Проблема с написанием книги: это гораздо больше работы, чем вы можете себе представить. Если вы хотите углубиться в C ++ 11 и выше, я предлагаю вам купить «Effective Modern C ++» Скотта Мейерса.
fredoverflow
77

Семантика перемещения основана на ссылках rvalue .
Значение r это временный объект, который будет уничтожен в конце выражения. В текущем C ++ значения r связаны только со constссылками. C ++ 1x допускает не- constзначимые ссылки, записанные T&&, которые являются ссылками на объекты-значения.
Так как значение r умрет в конце выражения, вы можете украсть его данные . Вместо того, чтобы копировать его в другой объект, вы перемещаете в него данные.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

В приведенной выше коде, со старыми компиляторами результат f()будет скопирован в xиспользовании X«s конструктора копирования. Если ваш компилятор поддерживает семантику перемещения и Xимеет конструктор перемещения, то он вызывается вместо этого. Поскольку его rhsаргумент является r-значением , мы знаем, что он больше не нужен, и можем украсть его значение.
Таким образом, значение перемещается из безымянного временного объекта , возвращаемого из f()в x(в то время как данные x, инициализированные пустым X, перемещаются во временные, которые будут уничтожены после назначения).

SBI
источник
1
обратите внимание, что это должно быть this->swap(std::move(rhs));потому, что именованные ссылки на rvalue являются lvalues
wmamrak
Это своего рода неправильно, на @ комментарий Tacyt в: rhsэто именующее в контексте X::X(X&& rhs). Вам нужно позвонить, std::move(rhs)чтобы получить значение, но это как бы делает ответ спорным.
Ашера
Что перемещает семантику для типов без указателей? Переместить семантику, работает ликая копия?
Гусев Слава
@ Гусев: Понятия не имею, о чем ты спрашиваешь.
sbi
60

Предположим, у вас есть функция, которая возвращает существенный объект:

Matrix multiply(const Matrix &a, const Matrix &b);

Когда вы пишете такой код:

Matrix r = multiply(a, b);

затем обычный компилятор C ++ создаст временный объект для результата multiply(), вызовет конструктор копирования для инициализации r, а затем уничтожит временное возвращаемое значение. Семантика перемещения в C ++ 0x позволяет вызывать «конструктор перемещения» для инициализацииr путем копирования его содержимого, а затем отбрасывать временное значение без необходимости его уничтожения.

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

Грег Хьюгилл
источник
2
Чем отличаются конструкторы перемещения и копирования?
Dicroce
1
@dicroce: они различаются по синтаксису, один выглядит как Matrix (const Matrix & src) (конструктор копирования), а другой выглядит как Matrix (Matrix && src) (конструктор перемещения), посмотрите мой основной ответ для лучшего примера.
snk_kid
3
@dicroce: один делает пустой объект, а другой делает копию. Если данные, хранящиеся в объекте, большие, их копирование может быть дорогим. Например, std :: vector.
Билли ОНил
1
@ kunj2aan: это зависит от вашего компилятора, я подозреваю. Компилятор может создать временный объект внутри функции, а затем переместить его в возвращаемое значение вызывающей стороны. Или, он может быть в состоянии непосредственно создать объект в возвращаемом значении, без необходимости использовать конструктор перемещения.
Грег Хьюгилл
2
@Jichao: Это оптимизация под названием RVO, см. Этот вопрос для получения дополнительной информации о разнице: stackoverflow.com/questions/5031778/…
Грег Хьюгилл,
30

Если вы действительно заинтересованы в хорошем и глубоком объяснении семантики перемещения, я настоятельно рекомендую прочитать оригинальную статью о них, «Предложение добавить поддержку семантики перемещения в язык C ++».

Это очень доступно и легко читается, и это превосходное доказательство преимуществ, которые они предлагают. На веб-сайте WG21 есть и другие, более свежие и актуальные статьи о семантике перемещения , но эта, пожалуй, самая прямолинейная, так как она подходит к вещам с точки зрения верхнего уровня и не вдавается в подробности языка.

Джеймс МакНеллис
источник
27

Семантика перемещения - это передача ресурсов, а не их копирование когда исходное значение больше никому не нужно.

В C ++ 03 объекты часто копируются, только чтобы быть уничтоженными или присвоенными, прежде чем какой-либо код снова использует это значение. Например, когда вы возвращаете значение по значению из функции - если только RVO не активируется - возвращаемое вами значение копируется в кадр стека вызывающей стороны, а затем выходит из области видимости и уничтожается. Это только один из многих примеров: см. Передачу по значению, когда исходный объект является временным, алгоритмы, подобные sortэтому, просто переставляют элементы, перераспределение, vectorкогда егоcapacity() превышении его и т. Д.

Когда такие пары копирования / уничтожения дороги, это обычно потому, что объекту принадлежит какой-то тяжеловесный ресурс. Например, vector<string>может иметь динамически распределенный блок памяти, содержащий массив stringобъектов, каждый из которых имеет свою собственную динамическую память. Копирование такого объекта является дорогостоящим: вы должны выделить новую память для каждого динамически распределяемого блока в источнике и скопировать все значения по всему. Затем вам нужно освободить всю память, которую вы только что скопировали. Однако перемещение большого vector<string>означает просто копирование нескольких указателей (которые относятся к динамическому блоку памяти) к месту назначения и обнуление их в источнике.

Дейв Абрахамс
источник
23

В простых (практических) терминах:

Копирование объекта означает копирование его «статических» членов и вызов newоператора для его динамических объектов. Правильно?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Тем не менее, чтобы двигаться объекта (я повторяю, с практической точки зрения) подразумевает только копирование указателей динамических объектов, а не создание новых.

Но разве это не опасно? Конечно, вы можете дважды уничтожить динамический объект (ошибка сегментации). Таким образом, чтобы избежать этого, вы должны «аннулировать» указатели источника, чтобы не уничтожить их дважды:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Хорошо, но если я перемещу объект, исходный объект станет бесполезным, нет? Конечно, но в определенных ситуациях это очень полезно. Наиболее очевидным является случай, когда я вызываю функцию с анонимным объектом (временный, объект rvalue, ..., вы можете вызывать его с разными именами):

void heavyFunction(HeavyType());

В этой ситуации анонимный объект создается, затем копируется в параметр функции, а затем удаляется. Итак, здесь лучше перемещать объект, потому что вам не нужен анонимный объект, и вы можете сэкономить время и память.

Это приводит к понятию "rvalue" ссылка. Они существуют в C ++ 11 только для определения, является ли полученный объект анонимным или нет. Я думаю, вы уже знаете, что «lvalue» является присваиваемой сущностью (левая часть =оператора), поэтому вам нужна именованная ссылка на объект, чтобы иметь возможность выступать в качестве lvalue. Значение r с точностью до наоборот, объект без именованных ссылок. Из-за этого анонимный объект и rvalue являются синонимами. Так:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

В этом случае, когда объект типа A должен быть «скопирован», компилятор создает ссылку lvalue или ссылку rvalue в зависимости от того, назван переданный объект или нет. Если нет, вызывается ваш конструктор перемещения, и вы знаете, что объект является временным, и вы можете перемещать его динамические объекты вместо их копирования, экономя пространство и память.

Важно помнить, что «статические» объекты всегда копируются. Нет способов «переместить» статический объект (объект в стек, а не в кучу). Таким образом, различие «перемещение» / «копия», когда объект не имеет динамических членов (прямо или косвенно), не имеет значения.

Если ваш объект сложный и деструктор имеет другие вторичные эффекты, такие как вызов функции библиотеки, вызов других глобальных функций или что-то еще, возможно, лучше сигнализировать о движении с флагом:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Итак, ваш код короче (вам не нужно делать nullptr назначение для каждого динамического члена) и более общий.

Другой типичный вопрос: в чем разница между A&&иconst A&& ? Конечно, в первом случае вы можете изменить объект, а во втором нет, но практический смысл? Во втором случае вы не можете изменить его, поэтому у вас нет способов сделать объект недействительным (кроме как с помощью изменяемого флага или чего-то подобного), и нет никакого практического различия для конструктора копирования.

А что такое идеальная пересылка ? Важно знать, что «ссылка на значение» является ссылкой на именованный объект в «области действия вызывающего». Но в реальной области действия ссылка на rvalue является именем объекта, поэтому она действует как именованный объект. Если вы передаете ссылку на rvalue другой функции, вы передаете именованный объект, поэтому объект не воспринимается как временный объект.

void some_function(A&& a)
{
   other_function(a);
}

Объект aбудет скопирован в фактический параметр other_function. Если вы хотите, чтобы объект aпродолжал обрабатываться как временный объект, вы должны использовать std::moveфункцию:

other_function(std::move(a));

С помощью этой строки std::moveприведёт aк rvalue и other_functionполучит объект как безымянный объект. Конечно, если other_functionнет специфической перегрузки для работы с неназванными объектами, это различие не важно.

Это идеальная пересылка? Нет, но мы очень близки. Идеальная пересылка полезна только для работы с шаблонами, с целью сказать: если мне нужно передать объект в другую функцию, мне нужно, чтобы, если я получил именованный объект, объект был передан как именованный объект, а когда нет, Я хочу передать его как безымянный объект:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Это сигнатура прототипной функции, которая использует совершенную пересылку, реализованную в C ++ 11 с помощью std::forward. Эта функция использует некоторые правила создания шаблона:

 `A& && == A&`
 `A&& && == A&&`

Таким образом, если Tl - значение ссылки на A( T = A &), aтакже ( A & && => A &). Если Tэто также ссылка на значение A, aтакже (A && && => A &&). В обоих случаях aэто именованный объект в реальной области видимости, но Tсодержит информацию о его «ссылочном типе» с точки зрения области действия вызывающей стороны. Эта информация ( T) передается в качестве параметра шаблона, forwardа 'a' перемещается или нет в соответствии с типом T.

Peregring-лк
источник
20

Это похоже на семантику копирования, но вместо того, чтобы дублировать все данные, которые вы получаете, чтобы украсть данные из объекта, из которого «перемещаются».

Терри Махаффи
источник
13

Вы знаете, что означает семантика копирования? это означает, что у вас есть типы, которые можно копировать, для пользовательских типов, которые вы определяете, это либо покупайте явно написав конструктор копирования и оператор присваивания, либо компилятор генерирует их неявно. Это сделает копию.

Семантика перемещения - это в основном пользовательский тип с конструктором, который принимает ссылку на r-значение (новый тип ссылки с использованием && (да, два амперсанда)), который не является константным, это называется конструктором перемещения, то же самое относится и к оператору присваивания. Так что же делает конструктор перемещения, вместо того, чтобы копировать память из аргумента источника, он «перемещает» память из источника в место назначения.

Когда бы вы хотели это сделать? Например, хорошо std :: vector, например, вы создали временный std :: vector и возвращаете его из функции, скажем:

std::vector<foo> get_foos();

При возврате функции у вас будут накладные расходы от конструктора копирования, если (и это будет в C ++ 0x) std :: vector имеет конструктор перемещения вместо копирования, он может просто установить его указатели и динамически выделить «перемещение» память на новый экземпляр. Это похоже на семантику передачи права собственности с помощью std :: auto_ptr.

snk_kid
источник
1
Я не думаю, что это отличный пример, потому что в этих примерах возвращаемого значения функции Оптимизация возвращаемого значения, вероятно, уже исключает операцию копирования.
Zan Lynx
7

Чтобы проиллюстрировать необходимость семантики перемещения , давайте рассмотрим этот пример без семантики перемещения:

Вот функция, которая принимает объект типа Tи возвращает объект того же типа T:

T f(T o) { return o; }
  //^^^ new object constructed

Вышеуказанная функция использует вызов по значению что означает , что , когда эта функция называется объект должен быть построен для использования этой функции.
Поскольку функция также возвращает значение , для нового значения создается другой новый объект:

T b = f(a);
  //^ new object constructed

Были созданы два новых объекта, один из которых является временным объектом, который используется только на время выполнения функции.

Когда новый объект создается из возвращаемого значения, вызывается конструктор копирования для копирования содержимого временного объекта в новый объект b. После завершения функции временный объект, используемый в функции, выходит из области видимости и уничтожается.


Теперь давайте рассмотрим, что делает конструктор копирования .

Сначала необходимо инициализировать объект, а затем скопировать все соответствующие данные из старого объекта в новый.
В зависимости от класса, может быть, это контейнер с очень большим количеством данных, тогда это может представлять много времени и использования памяти

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

С семантикой перемещения теперь можно сделать большую часть этой работы менее неприятной, просто перемещая данные, а не копируя.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Перемещение данных включает в себя повторное связывание данных с новым объектом. И никакой копии не происходит вообще.

Это достигается с помощью rvalueссылки. Ссылка работает довольно много , как ссылки с одним важным отличием: Rvalue ссылка может быть перемещена
rvaluelvalue
и именующий не может.

От cppreference.com :

Чтобы сделать возможным строгое исключение, пользовательские конструкторы перемещения не должны генерировать исключения. Фактически, стандартные контейнеры обычно используют std :: move_if_noexcept для выбора между перемещением и копированием, когда необходимо перемещать элементы контейнера. Если предусмотрены конструкторы копирования и перемещения, разрешение перегрузки выбирает конструктор перемещения, если аргумент является значением rvalue (либо prvalue, например, безымянное временное значение, либо xvalue, например, результат std :: move), и выбирает конструктор копирования, если аргумент является lvalue (именованный объект или функция / оператор, возвращающий ссылку на lvalue). Если предоставляется только конструктор копирования, все категории аргументов выбирают его (при условии, что он принимает ссылку на const, поскольку rvalues ​​может связываться с ссылками на const), что делает копирование запасного варианта для перемещения, когда перемещение недоступно. Во многих ситуациях конструкторы перемещения оптимизируются, даже если они будут вызывать наблюдаемые побочные эффекты, см. Раздел «Разрешение копирования». Конструктор называется «конструктором перемещения», когда он принимает в качестве параметра ссылку на значение. Он не обязан что-либо перемещать, класс не обязан иметь ресурс для перемещения, и «конструктор перемещения» может не иметь возможности перемещать ресурс, как в допустимом (но, возможно, нецелесообразном) случае, когда параметр является ссылка на постоянное значение (const T &&).

Андреас Д.М.
источник
7

Я пишу это, чтобы убедиться, что я правильно понимаю.

Семантика перемещения была создана, чтобы избежать ненужного копирования больших объектов. Бьярн Страуструп в своей книге «Язык программирования C ++» использует два примера, где по умолчанию происходит ненужное копирование: один - замена двух больших объектов и два - возврат большого объекта из метода.

Обмен двух больших объектов обычно включает в себя копирование первого объекта во временный объект, копирование второго объекта в первый объект и копирование временного объекта во второй объект. Для встроенного типа это очень быстро, но для больших объектов эти три копии могут занять много времени. «Назначение перемещения» позволяет программисту переопределить поведение копирования по умолчанию и вместо этого поменять местами ссылки на объекты, что означает, что копирование вообще не выполняется и операция подкачки выполняется намного быстрее. Назначение перемещения может быть вызвано путем вызова метода std :: move ().

Возврат объекта из метода по умолчанию включает в себя создание копии локального объекта и связанных с ним данных в месте, доступном для вызывающей стороны (поскольку локальный объект недоступен для вызывающей стороны и исчезает по завершении метода). Когда возвращается встроенный тип, эта операция выполняется очень быстро, но если возвращается большой объект, это может занять много времени. Конструктор перемещения позволяет программисту переопределить это поведение по умолчанию и вместо этого «повторно» использовать данные кучи, связанные с локальным объектом, указав объекту, возвращаемому вызывающей стороне, для кучи данных, связанных с локальным объектом. Таким образом, копирование не требуется.

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

Крис Б
источник
«Назначение перемещения» позволяет программисту переопределить поведение копирования по умолчанию и вместо этого поменять местами ссылки на объекты, что означает, что копирование вообще не выполняется и операция подкачки выполняется намного быстрее ». - эти претензии неоднозначны и вводят в заблуждение. Чтобы поменять местами два объекта xи y, вы не можете просто «ссылки своп на объекты» ; может случиться так, что объекты содержат указатели, которые ссылаются на другие данные, и эти указатели можно поменять местами, но операторы перемещения не обязаны ничего менять. Они могут стереть данные из удаленного объекта, а не сохранить в нем данные dest.
Тони Делрой
Вы можете написать swap()без семантики перемещения. «Назначение перемещения можно вызвать, вызвав метод std :: move ()». - иногда необходимо использовать std::move()- хотя это на самом деле ничего не перемещает - просто позволяет компилятору знать, что аргумент является подвижным, иногда std::forward<>()(с пересылкой ссылок), а в других случаях компилятор знает, что значение может быть перемещено.
Тони Делрой
-2

Вот ответ из книги Бьярна Страуструпа «Язык программирования C ++». Если вы не хотите видеть видео, вы можете увидеть текст ниже:

Посмотрите на этот фрагмент. Возврат из оператора + включает в себя копирование результата из локальной переменной resв место, где вызывающий может получить к нему доступ.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Мы действительно не хотели копировать; мы просто хотели получить результат из функции. Поэтому нам нужно переместить Вектор, а не копировать его. Мы можем определить конструктор перемещения следующим образом:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&& означает «ссылка на rvalue» и является ссылкой, с которой мы можем связать значение rvalue. «rvalue» 'предназначен для дополнения «lvalue», что примерно означает «что-то, что может появиться в левой части назначения». Таким образом, rvalue означает примерно «значение, которое вы не можете назначить», такое как целое число, возвращаемое вызовом функции, и resлокальная переменная в operator + () для Vectors.

Теперь заявление return res;не будет копироваться!

Роб Пей
источник