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


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


1343


источник


Ответы:


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

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

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

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

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        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, потому что базовые строковые объекты не имеют имен, поэтому у клиента нет возможности снова проверить их на более поздний момент времени. rvalues ​​обозначают временные объекты, которые уничтожаются на следующей точке с запятой (точнее: в конце полного выражения, которое лексически содержит rvalue). Это важно, поскольку во время инициализации bа также c, мы могли бы делать все, что захотим, с исходной строкой, и клиент не мог сказать разницы !

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

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

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

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

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

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

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

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

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

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

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


1973



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

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

Введение

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

  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_ptrв C ++ 11. Промежуточные программисты на С ++, вероятно, по крайней мере знакомы с 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заключается в том, что синтаксически выглядит как копия на самом деле. Попытка вызвать функцию-член на перемещенном 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по стоимости, создавая тем самым свежие временные auto_ptrобъект каждый раз, когда он вызывается. aявляется примером именующий , в то время как make_triangle()является примером Rvalue ,

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

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

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

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

Ссылки Rvalue

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

Ответ C ++ 11 на эту проблему ссылки rvalue , Ссылка rvalue - это новый тип ссылки, который привязывается только к rvalues, а синтаксис - 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&&, Ограничение чтения из rvalues ​​не очень полезно.

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

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

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

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

some_function("hello world");

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

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

Полезный пример функции с 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является значением l, но параметр unique_ptr&& sourceможет быть привязана только к значениям r. Это именно то, что мы хотели; опасные движения никогда не должны быть скрытыми. Третья строка компилируется просто отлично, потому что make_triangle()является rvalue. Конструктор перемещения переносит право собственности с временного на 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выходит из сферы действия, автоматически освобождая старый ресурс.

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

Переход от lvalues

Иногда мы хотим перейти от lvalues. То есть, иногда мы хотим, чтобы компилятор обрабатывал lvalue, как если бы он был rvalue, поэтому он может вызывать конструктор перемещения, даже если он может быть потенциально опасным. С этой целью 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, даже если оно не является rvalue в традиционном смысле, называется xvalue (значение eXpiring). Традиционные значения были переименованы в prvalues (Чистые значения).

Оба значения и значения x являются значениями r. Xvalues ​​и lvalues ​​- оба glvalues (Обобщенные значения l). Отношения легче понять с помощью диаграммы:

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

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

C ++ 98 rvalues ​​известны как prvalues ​​в 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. Ссылки 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!
}

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

Перемещение в члены

Рано или поздно вы собираетесь написать такой код:

class Foo
{
    unique_ptr<Shape> member;

public:

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

В принципе, компилятор будет жаловаться, что parameterявляется lvalue. Если вы посмотрите на его тип, вы увидите ссылку rvalue, но ссылка 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&&для привязки к rvalues, потому что на первый взгляд это похоже на ссылку 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>&

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

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

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

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


870



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

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''. Если ваш компилятор поддерживает перемещение семантики и Xимеет конструктор move, тогда это называется. Поскольку rhsаргументом является Rvalue , мы знаем, что это не нужно больше, и мы можем украсть его ценность.
Таким образом, значение переехал из неназванного временного возврата из f()в x(в то время как данные x, инициализированный пустым X, перемещается во временное, которое будет уничтожено после назначения).


67



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

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

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

Matrix r = multiply(a, b);

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

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


44



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

Он очень доступен и легко читается, и он отлично подходит для преимуществ, которые они предлагают. Существуют и другие более свежие и современные документы о семантике перемещения, доступные на веб-сайт РГ21 , но этот, пожалуй, самый простой, поскольку он приближается к вещам с точки зрения верхнего уровня и не очень сильно разбирается в подробных деталях языка.


27



Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.


20



In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. Right?

class A
{
   int i, *p;

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

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

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. 
};

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. So:

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; }
};

In this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

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 */ }
};

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&&? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

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

The object a would be copied to the actual parameter of other_function. If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

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

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward. This function exploits some rules of template instantiation:

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

So, if T is a lvalue reference to A (T = A&), a also (A& && => A&). If T is a rvalue reference to A, a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information (T) is passed as template parameter to forward and 'a' is moved or not according to the type of T.


18



It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.


16



You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.


13



To illustrate the need for move semantics, let's consider this example without move semantics:

Here's a function that takes an object of type T and returns an object of the same type T:

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

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value, another new object is constructed for the return value:

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

Two new objects have been constructed, one of which is a temporary object that's only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.


Now, let's consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

// 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);
}

With move semantics it's now possible to make most of this work less unpleasant by simply moving the data rather than copying.

// 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);
}

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com:

To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. A constructor is called a 'move constructor' when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a 'move constructor' may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).


6



I'm writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book "The C++ Programming Language" uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A "move assignment" allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead "reuse" the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

In languages which do not allow the creation of local objects (that is, objects on the stack) these types of problems do not occur as all objects are allocated on the heap and are always accessed by reference.


3