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

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

вопрос задан 23.06.2010
dicroce
20874 репутация

11 ответов


  • 2088 рейтинг

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

    #include 
    #include 
    
    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, потому что базовые строковые объекты не имеют имен, поэтому у клиента нет возможности проверить их снова в более позднее время. Значения r обозначают временные объекты, которые уничтожаются в следующей точке с запятой (точнее: в конце полного выражения, которое лексически содержит значение r). Это важно, потому что во время инициализации 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? В былые времена C ++ 98 ответом был бы «конструктор копирования». В C ++ 0x компилятор выбирает между конструктором копирования и конструктором перемещения в зависимости от того, является ли аргумент оператора присваивания lvalue или rvalue.

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

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

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

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

    ответ дан fredoverflow, с репутацией 159315, 24.06.2010
  • 905 рейтинг

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

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

    Введение

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

    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. Если вы не знакомы с auto_ptr, его цель - гарантировать, что динамически размещаемый объект всегда освобождается, даже при исключениях:

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

    Необычная вещь о auto_ptr - это его «копирующее» поведение:

    auto_ptr a(new Triangle);
    
          +---------------+
          | triangle data |
          +---------------+
            ^
            |
            |
            |
      +-----|---+
      |   +-|-+ |
    a | p | | | |
      |   +---+ |
      +---------+
    
    auto_ptr 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 a(new Triangle);   // create triangle
    auto_ptr b(a);              // move a into b
    double area = a->area();           // undefined behavior
    

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

    auto_ptr make_triangle()
    {
        return auto_ptr(new Triangle);
    }
    
    auto_ptr c(make_triangle());      // move temporary into c
    double area = make_triangle()->area();   // perfectly safe
    

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

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

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

    Значения категории

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

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

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

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

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

    Rvalue ссылки

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

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

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

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

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

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

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

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

    void some_function(std::string&& r);
    
    some_function("hello world");
    

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

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

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

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

    template
    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 a(new Triangle);
    unique_ptr b(a);                 // error
    unique_ptr c(make_triangle());   // okay
    

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

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

    Операторы присваивания перемещения

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

        unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
        {
            if (this != &source)    // beware of self-assignment
            {
                delete ptr;         // release the old resource
    
                ptr = source.ptr;   // acquire the new resource
                source.ptr = nullptr;
            }
            return *this;
        }
    };
    

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

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

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

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

    Перемещение от lvalues ​​

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

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

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

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

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

    Xvalues ​​

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    class Foo
    {
        unique_ptr member;
    
    public:
    
        Foo(unique_ptr&& parameter)
        : member(parameter)   // error
        {}
    };
    

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

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

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

    class Foo
    {
        unique_ptr member;
    
    public:
    
        Foo(unique_ptr&& 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
    void foo(T&&);
    

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

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

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

    T & amp; это не ссылка на значение, а ссылка на пересылку. Он также привязывается к lvalue, и в этом случае T и T&& являются ссылками lvalue.

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

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

    Выполнение хода

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

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

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

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

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

    ответ дан fredoverflow, с репутацией 159315, 18.07.2012
  • 70 рейтинг

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

    ответ дан sbi, с репутацией 161246, 23.06.2010
  • 51 рейтинг

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

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

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

    Matrix r = multiply(a, b);
    

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

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

    ответ дан Greg Hewgill, с репутацией 645957, 23.06.2010
  • 27 рейтинг

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

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

    ответ дан James McNellis, с репутацией 282034, 23.06.2010
  • 23 рейтинг

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

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

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

    ответ дан Dave Abrahams, с репутацией 6114, 8.04.2012
  • 20 рейтинг

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

    Копирование объекта означает копирование его «статических» членов и вызов оператора 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());
    

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

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

    class A
    {
       int i, *p;
    
    public:
       // Copy
       A(const A& a) : i(a.i), p(new int(*a.p)) {}
    
       // Movement (&& means "rvalue reference to")
       A(A&& a) : i(a.i), p(a.p)
       {
          a.p = nullptr;
       }
    
       ~A() { delete p; }
    };
    

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

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

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

    class Heavy
    {
       bool b_moved;
       // staff
    
    public:
       A(const A& a) { /* definition */ }
       A(A&& a) : // initialization list
       {
          a.b_moved = true;
       }
    
       ~A() { if (!b_moved) /* destruct object */ }
    };
    

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

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

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

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

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

    other_function(std::move(a));
    

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

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

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

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

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

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

    ответ дан Peregring-lk, с репутацией 4084, 18.08.2013
  • 17 рейтинг

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

    ответ дан Terry Mahaffey, с репутацией 9903, 23.06.2010
  • 13 рейтинг

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

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

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

    std::vector get_foos();
    

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

    ответ дан snk_kid, с репутацией 2882, 23.06.2010
  • 6 рейтинг

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

    Вот функция, которая принимает объект типа 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), что делает копирование запасного варианта для перемещения, когда перемещение недоступно. Во многих ситуациях конструкторы перемещения оптимизируются, даже если они будут вызывать наблюдаемые побочные эффекты, см. Раздел «Разрешение копирования». Конструктор называется «конструктором перемещения», когда он принимает в качестве параметра ссылку на значение. Он не обязан что-либо перемещать, класс не обязан иметь ресурс для перемещения, и «конструктор перемещения» может не иметь возможности перемещать ресурс, как в допустимом (но, возможно, нецелесообразном) случае, когда параметр является ссылка на постоянное значение (const T & amp;).

    ответ дан Andreas DM, с репутацией 6048, 25.02.2016
  • 4 рейтинг

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

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

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

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

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

    ответ дан Chris B, с репутацией 323, 18.11.2016