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

1432

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

  • 17
    Я нашел [статью Эли Бендерского в блоге] ( eli.thegreenplace.net/2011/12/15/… ) о lvalues и rvalues в C и C ++ довольно информативным. Он также упоминает ссылки на rvalue в C ++ 11 и представляет их с небольшими примерами.
  • 14
    Экспозиция Алекса Аллена на эту тему очень хорошо написана.
Показать ещё 2 комментария
Теги:
c++11
move-semantics
c++-faq

11 ответов

2122
Лучший ответ

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

#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 мы могли делать все, что хотели, с исходной строкой, и клиент не мог отличить эту информацию!

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

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

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

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

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

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

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

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

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

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

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

  • 76
    Отличный ответ, прояснили. Есть ссылки на то, что вы оставили?
  • 35
    @ Но если мой ctor получает rvalue, который никогда не может быть использован позже, зачем мне вообще нужно оставлять его в согласованном / безопасном состоянии? Вместо установки that.data = 0, почему бы просто не оставить это?
Показать ещё 45 комментариев
871

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

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

Введение

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

  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 до 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 после его перемещения из:

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() является примером rvalue.

Перемещение с 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, которые могут (все значения r типы классов с оператором присваивания).

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" является lvalue типа const char[12]. Поскольку существует неявное преобразование из const char[12] через const char* в std::string, создается временная 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 является значением lvalue, но параметр 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 просто 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 (Pure rvalues).

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

        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
}

Почему конструктор move принимает result lvalue в качестве аргумента? Объем 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, а не 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&& также связывается с 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.

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

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

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

  • 179
  • 24
    Существует третья причина, по которой важна семантика перемещения: безопасность исключений. Часто, когда операция копирования может генерировать (потому что ей нужно распределить ресурсы, а выделение может произойти сбой), операция перемещения может быть безудержной (потому что она может передать владение существующим ресурсам вместо выделения новых). Операции, которые не могут завершиться неудачей, всегда хороши, и это может иметь решающее значение при написании кода, который обеспечивает гарантии исключений.
Показать ещё 17 комментариев
69

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

  • 1
    обратите внимание, что это должно быть this->swap(std::move(rhs)); потому что именованные ссылки rvalue являются lvalues
  • 0
    Это неправильно, согласно комментарию @ Tacyt: rhs - это lvalue в контексте X::X(X&& rhs) . Вам нужно вызвать std::move(rhs) чтобы получить значение, но это как бы делает ответ спорным.
Показать ещё 2 комментария
52

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

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

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

Matrix r = multiply(a, b);

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

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

  • 2
    Чем отличаются конструкторы перемещения и копирования?
  • 1
    @dicroce: они различаются по синтаксису, один выглядит как Matrix (const Matrix & src) (конструктор копирования), а другой выглядит как Matrix (Matrix && src) (конструктор перемещения), посмотрите мой основной ответ для лучшего примера.
Показать ещё 6 комментариев
28

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

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

27

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

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

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

21

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

Копирование объекта означает копирование его "статических" элементов и вызов оператора 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". Они существуют в С++ 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 в соответствии с именем или именем переданного объекта. Когда нет, ваш конструктор move вызывается, и вы знаете, что объект является временным, и вы можете перемещать его динамические объекты, а не копировать их, экономя пространство и память.

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

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

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

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

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

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

19

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

13

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

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

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

std::vector<foo> get_foos();

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

  • 1
    Я не думаю, что это отличный пример, потому что в этих примерах возвращаемого значения функции Оптимизация возвращаемого значения, вероятно, уже исключает операцию копирования.
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 работает очень похоже на ссылку lvalue с одним важным отличием:
ссылку rvalue можно перемещать, а значение lvalue не может.

От cppreference.com:

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

5

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню