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

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

11 ответов


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

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

что мы здесь сделали? Вместо глубокого копирования данных кучи мы просто скопировали указатель, а затем установили исходный указатель на null. Фактически, мы" украли " данные, которые изначально принадлежали исходной строке. Опять же, ключевым моментом является то, что ни при каких обстоятельствах клиент не мог обнаружить, что источник был изменен. Так как мы не действительно сделайте копию здесь, мы называем этот конструктор "конструктором перемещения". Его задача-перемещать ресурсы с одного объекта на другой, а не копировать их.

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

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

Да, это все? - Где ссылка на rvalue?- можешь спросить. - Здесь он нам не нужен! это мой ответ:)

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

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

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

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

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


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

т. Lavavej Стефан взял время обеспечить ценную обратную связь. Большое спасибо, Стефан!

введение

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

  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. Промежуточные программисты 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 to b. Говорят также "a is переехали в b" или "треугольник двигался с a to 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 это то, что синтаксически выглядит как копия, на самом деле является движением. Попытка вызвать функцию-член на перемещенном-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 по значению, таким образом, создавая новый временный auto_ptr объект каждый раз, когда он вызывается. a пример lvalue, а make_triangle() пример правосторонним значением.

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

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

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

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

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

some_function("hello world");

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

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

полезный пример функции с как-то изменилось; когда поток управления возвращается на вызывающего абонента, 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. Перемещение исключительно выполняется конструктором move, а не std::move, а не просто привязкой rvalue к ссылке 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&& также связывается с значения lvalue:

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>&

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

T&& не является ссылкой rvalue,А ссылкой для пересылки. Он также привязывается к lvalues, и в этом случае T и T&& оба являются ссылками lvalue.

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

#include <type_traits>

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

реализация move

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

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

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


семантика перемещения основана на rvalue references.
Rvalue-это временный объект, который будет уничтожен в конце выражения. В текущем C++ rvalues привязываются только к const ссылки. C++1x позволит не -const rvalue ссылки, пишется 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() is скопировал на x используя Xконструктор копирования. Если ваш компилятор поддерживает семантику перемещения и X есть движение-конструктор, то вызывается. С его


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

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

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

Matrix r = multiply(a, b);

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

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


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

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


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

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

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


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

копирование объекта означает копирование его "статических" членов и вызов 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. 
};

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

void heavyFunction(HeavyType());

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

это приводит к понятию ссылки "rvalue". Они существуют в C++11 только для обнаружения, является ли полученный объект анонимным или нет. Я думаю, вы уже знаете, что "lvalue" является назначаемой сущностью (левая часть = оператор), поэтому вам нужна именованная ссылка на объект, чтобы быть способным действовать как lvalue. Ля rvalue-это прямо противоположное, объект без именованных ссылок. Из-за этого анонимный объект и 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 является именем объекта, поэтому она действует как именованный объект. Если вы передаете ссылку 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&&`

Итак, если T является ссылкой lvalue на A (T = а&), a и (A& & & = > A&). Если T является ссылкой rvalue на A, a также (A&& & & = > A&&). В обоих случаях a является именованным объектом в фактической области, но T содержит информацию о своем "ссылочном типе" С точки зрения области вызывающего абонента. Эта информация (T) передается в качестве параметра шаблона forward и "a" перемещается или нет в соответствии с типом T.


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


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

семантика перемещения-это в основном пользовательский тип с конструктором, который принимает ссылку на r-значение (новый тип ссылки с использованием & & (да два амперсанда)), который не является const, это называется конструктором перемещения, то же самое касается оператора присваивания. Итак, что делает конструктор перемещения, вместо копирования памяти из исходного аргумента он "перемещает" память из источника в место назначения.

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

std::vector<foo> get_foos();

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


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

вот функция, которая принимает объект типа 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 ссылка работает в значительной степени как lvalue ссылка с одним важным отличием:
Ан значение rvalue ссылка может быть перемещена и lvalue не может.

от cppreference.com:

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


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

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

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

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

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