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

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


person dicroce    schedule 23.06.2010    source источник
comment
Я нашел [статью в блоге Эли Бендерски] (eli.thegreenplace.net/2011/12/15/) о lvalue и rvalue в C и C ++ довольно информативно. Он также упоминает ссылки на rvalue в C ++ 11 и представляет их небольшими примерами.   -  person Nils    schedule 18.07.2012
comment
Изложение Алекса Аллена на тема написана очень хорошо.   -  person Patrick Sanan    schedule 12.12.2013
comment
Каждый год или около того мне интересно, в чем заключается новая семантика перемещения в C ++, я гуглил и перехожу на эту страницу. Читаю ответы, мозг отключается. Я возвращаюсь в C и все забываю! Я в тупике.   -  person sky    schedule 02.07.2018
comment
@sky Рассмотрим std :: vector ‹› ... Где-то в куче есть указатель на массив. Если вы копируете этот объект, необходимо выделить новый буфер и скопировать данные из буфера в новый буфер. Есть ли обстоятельства, при которых можно было бы просто украсть указатель? Ответ - ДА, если компилятор знает, что объект временный. Семантика перемещения позволяет вам определять, как внутренности ваших классов могут быть перемещены и помещены в другой объект, когда компилятор знает, что объект, из которого вы перемещаетесь, вот-вот исчезнет.   -  person dicroce    schedule 10.09.2018
comment
Единственная ссылка, которую я могу понять: learncpp.com/cpp-tutorial/, т.е. исходное обоснование семантики перемещения основано на интеллектуальных указателях.   -  person jw_    schedule 03.11.2019


Ответы (11)


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

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

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

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

    ~string()
    {
        delete[] data;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

person fredoverflow    schedule 24.06.2010
comment
@ Но если мой ctor получает rvalue, которое никогда не может быть использовано позже, почему мне вообще нужно беспокоиться о том, чтобы оставить его в согласованном / безопасном состоянии? Вместо того, чтобы устанавливать that.data = 0, почему бы просто не оставить это в покое? - person einpoklum; 16.07.2013
comment
@einpoklum Потому что без that.data = 0 персонажи будут уничтожены слишком рано (когда временное умрет), а также дважды. Вы хотите украсть данные, а не делиться ими! - person fredoverflow; 17.07.2013
comment
@einpoklum Регулярно запланированный деструктор все еще запускается, поэтому вы должны убедиться, что состояние исходного объекта после перемещения не вызывает сбой. Лучше убедиться, что исходный объект также может быть получателем назначения или другой записи. - person CTMacUser; 02.09.2013
comment
Ваше утверждение: обнулить исходный объект таким образом - это нормально, потому что у клиента нет возможности снова проверить объект. Нужно ли обнулять, потому что мы все равно не можем получить к нему доступ, и если определение функции завершится, указатель также будет уничтожен? - person Pranit Kothari; 03.10.2013
comment
@pranitkothari Да, все объекты должны быть уничтожены, даже если объекты удалены. И поскольку мы не хотим, чтобы массив char удалялся, когда это происходит, мы должны установить указатель на null. - person fredoverflow; 03.10.2013
comment
@ Даан, это неправильно. Что ж, вы правы в том, что показанный «простой» оператор не является оператором присваивания перемещения. Это оператор копирования-присваивания (который, кстати, также может использоваться для ходов). Но остальное не так. В качестве простого примера счетчика рассмотрим класс foo с членом unique_ptr и другой член типа с назначением копии, но без назначения перемещения. Как вы думаете, у foo не будет оператора присваивания перемещения? Также учтите, что встроенные типы не имеют операторов присваивания перемещения. Означает ли это, что любой класс с членом int не имеет оператора присваивания перемещения? - person R. Martinho Fernandes; 13.12.2013
comment
@Daan GCC так себя не ведет. Clang делает. Придется попросить цитату;). (А про целые ни слова?) - person R. Martinho Fernandes; 16.12.2013
comment
Почему вы увеличили размер этой строки? - size_t size = strlen (that.data) + 1; - person adnako; 16.06.2014
comment
@adnako Потому что strlen не считает завершающий символ NUL. - person fredoverflow; 16.06.2014
comment
Почему эта семантика называется перемещением? Это имя предполагает, что какой-то объект в памяти действительно перемещается (что бы это ни значило), но это просто передача права собственности на данные. - person corazza; 05.08.2014
comment
@jco Согласен. Семантика передачи, вероятно, была бы лучшим выбором :) - person fredoverflow; 07.08.2014
comment
@FredOverflow Я рад, что вы согласны, я боялся, что что-то неправильно понял. - person corazza; 07.08.2014
comment
@FredOverflow что мешает string получить оператор неявного присваивания перемещения? Это единственное, что меня сбивает с толку. - person user1520427; 23.11.2014
comment
@ user1520427 Как только программист сам объявит одну из специальных функций-членов (конструктор копирования / перемещения, назначение копирования / перемещения, деструктор), ни одна из них не будет неявно сгенерирована. И в этом случае неявно сгенерированный оператор присваивания перемещения все равно поступит неправильно, потому что data = std::move(that.data) просто скопирует указатель. Правильно, перемещение указателя (или любого другого скалярного типа данных) имеет тот же эффект, что и копирование. - person fredoverflow; 23.11.2014
comment
спасибо хорошее объяснение, даже если ваша копия ctr не безопасна для исключений. - person jambodev; 15.04.2015
comment
@jambodev Что ты имеешь в виду? Может memcpy вызвать исключение? - person fredoverflow; 15.04.2015
comment
@fredoverflow нет, но новый может. - person jambodev; 15.04.2015
comment
@jambodev Итак? Если new char[size] выкидывает, где утечка? - person fredoverflow; 15.04.2015
comment
@fredoverflow Но я не говорил, что у него утечка. - person jambodev; 15.04.2015
comment
@jambodev Итак, что вы предлагаете делать, если new char[size] выкидывает. В чем именно проблема и как ее исправить? - person fredoverflow; 15.04.2015
comment
@jambodev копирующий ctor не выполняет никаких деструктивных действий, и никакие исключения не могут быть созданы после завершения единственного действия, которое может потребоваться отменить (это распределение), поэтому я не понимаю, как это не может быть безопасным в отношении исключений. - person R. Martinho Fernandes; 15.04.2015
comment
@fredoverflow, это, вероятно, не вызывает никаких проблем в вашем примере, поскольку размер не является членом класса, если это так, вы изменили размер, но не данные. Я хотел сказать, что лучше всего инициализировать данные в списке инициализации. - person jambodev; 15.04.2015
comment
@Andy Стандарт C ++ специально позволяет оптимизировать конструктор копирования! - person fredoverflow; 29.06.2015
comment
@fredoverflow О, хорошо - извините, я все еще нахожу это запутанным - означает ли это, что во многих случаях реализация конструктора перемещения не требуется? т.е. вы говорите, что конструктор копирования обычно будет оптимизирован в случае string a(b + c), даже если конструктора перемещения нет? - person Andy; 30.06.2015
comment
@Andy Да, компиляторы занимаются этим десятилетиями. - person fredoverflow; 30.06.2015
comment
@fredoverflow хорошо. В таком случае, когда действительно будет полезно написать конструктор перемещения? Поскольку, если я не понимаю, конструктор копирования будет обрабатывать любой случай, о котором я могу думать, так же оптимально? - person Andy; 01.07.2015
comment
@Andy: Реализуйте конструктор перемещения, когда перемещение более эффективно, чем копирование. В сложном объекте копия может быть очень глубокой и иметь много накладных расходов, в то время как перемещение может быть очень тривиальным. - person Remy Lebeau; 30.07.2015
comment
@RemyLebeau Я понимаю, что копирование глубокого объекта будет стоить дорого, но я все еще не понимаю, в каких случаях конструктор копирования не будет оптимизирован (т.е. случаи, когда конструктор перемещения необходим для улучшения представление) - person Andy; 30.07.2015
comment
Как компилятор узнает, что исходный код должен оставаться нетронутым? - person HelloGoodbye; 04.09.2015
comment
Таким образом, после кражи буфера из rvalue компилятор вызовет свой деструктор в конце выражения, вызывая delete [] для nullptr. Для меня это не имеет смысла. - person Virus721; 23.11.2015
comment
@ Virus721 Что именно для вас не имеет смысла? Ожидаете ли вы, что компилятор вообще не будет вызывать деструктор? Это было бы неправильно, потому что перемещенный объект все еще является объектом. При перемещении от объекта его срок службы не заканчивается. - person fredoverflow; 24.11.2015
comment
@ Virus721 delete[] на nullptr определен стандартом C ++ как запретный. - person fredoverflow; 24.11.2015
comment
Значит, нам все-таки не нужно создавать оператор присваивания перемещения? - person ozgur; 25.11.2015
comment
Этот пример тоже хорош. cppreference - person Damian; 15.04.2016
comment
Отличный пример! Я просматривал ваш пример шаг за шагом, но мне так и не удалось вызвать конструктор перемещения. Это b / c копирования? - person joshbodily; 25.05.2016
comment
@joshbodily Да, компиляторы C ++ очень умные :) Вы используете g ++ или clang? Вы пробовали -fno-elide-constructors? - person fredoverflow; 25.05.2016
comment
@fredoverflow. Думаю, я использовал clang: /. Я заставил его работать с этим флагом, но это было до того, как я прочитал другой учебник, в котором упоминалось, что вы можете принудительно использовать семантику перемещения с std::move. Спасибо! - person joshbodily; 25.05.2016
comment
@fredoverflow: Приятно видеть, как operator = больше не нужно проверять самоназначение. - person ViFI; 29.06.2016
comment
Я целую вечность просматривал страницы, чтобы найти хорошее объяснение по этой теме, и ваш ответ, безусловно, лучший из тех, что я читал. Отличная работа! - person binaryguy; 22.07.2017
comment
@binaryguy И я рад, что даже спустя 7 лет этот ответ все еще кажется полезным для моих коллег-программистов по всему миру :) - person fredoverflow; 22.07.2017
comment
В конструкторе перемещения не следует ли поменять местами данные и this.data вместо того, чтобы просто установить для that.data значение null, потому что в противном случае массив данных до назначения никогда не будет удален? - person Pinch; 25.07.2018
comment
@Pinch Вы путаете конструкторы и операторы присваивания? this.data не имеет детерминированного значения до построения, поэтому чтение из него во время свопа будет UB. - person fredoverflow; 25.07.2018
comment
Замечательное объяснение, у меня просто проблемы с клиентом, у меня нет возможности снова проверить объект, о котором вы упоминали несколько раз. Пример здесь, кажется, предполагает, что печатать строку после двигаясь от этого - person 463035818_is_not_a_number; 12.09.2018
comment
Может быть, было бы лучше memcpy(data, p, size * sizeof(char));? Я знаю, что char - это 1 байт, и так было давно, но с интернационализацией и интересом к поддержке большего количества мировых систем письма, возможно, так будет не так долго. - person Apollys supports Monica; 14.09.2018
comment
Отличный ответ. Некоторое время я пытался понять rvalue и lvalue, и это проясняет ситуацию. Спасибо. - person SubMachine; 02.06.2019
comment
@ previouslyknownas_463035818 у клиента нет возможности снова проверить объект, ссылаясь именно на значения, созданные x + y и some_function_returning_a_string(), а не на перемещенные строки или объекты в целом. Поскольку эти значения действительно временны, они нигде не хранятся, они никуда не попадают, кроме как прямо в конструктор копирования и никогда не могут быть проверены клиентом. - person Luke; 07.10.2019
comment
Ответ содержит ссылку на идиому копирования и обмена. В этой ссылке упоминается, что мы не можем использовать std :: swap для определения назначения копии, потому что сам std :: swap определяется в терминах назначения копии. Однако этот ответ использует std :: swap. Итак, какой из них правильный? - person user118967; 30.03.2021
comment
@ user118967 Мы меняем местами char* указатели this->data и that.data, не string объекты *this и that. Назначение указателям char* не определяется в терминах string назначения объекта. - person fredoverflow; 02.04.2021
comment
@fredoverflow, спасибо за ответ, но я не смог его понять. Я спрашивал об использовании std :: swap в этом посте по сравнению с заявлением (stackoverflow.com/a/3279550/3358488) что std :: swap работать не будет. Я не совсем понимаю, какое отношение к этому имеет то, что вы указываете. - person user118967; 04.04.2021
comment
хорошее объяснение, что происходит, когда мы сами не управляем своей памятью (например, нужны ли мне ссылки на rvalue, семантика перемещения и т. д., если я унаследован от строкового класса выше?) - person Imre; 05.05.2021
comment
Но разве x + y не будет храниться ни в стеке, ни в разделе .rodata? Какова гарантия того, что вы можете освободить этот указатель в деструкторе, если используется такая реализация конструктора перемещения? - person Ivankovich; 15.06.2021

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

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

Вступление

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

  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<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, вызывая неопределенное поведение. С другой стороны, переход от значений rvalue, таких как make_triangle(), совершенно безопасен, потому что после того, как конструктор копирования выполнил свою работу, мы не можем снова использовать временное значение. Нет выражения, которое обозначает указанное временное; если мы просто снова напишем make_triangle(), мы получим другое временное. Фактически, перемещенный временный объект уже пропал в следующей строке:

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

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

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

Ссылки на Rvalue

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

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

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

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

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

Ссылка 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" - это l-значение типа const char[12]. Поскольку существует неявное преобразование из const char[12] через const char* в std::string, создается временный объект типа std::string, и r привязан к этому временному объекту. Это один из случаев, когда различие между rvalue (выражениями) и временными значениями (объектами) немного размыто.

Конструкторы перемещения

Полезным примером функции с параметром 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, но он может быть предоставлен только с rvalue:

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

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

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

Переход от lvalues

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

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

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

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

std::move(some_lvalue) преобразует lvalue в rvalue, таким образом разрешая последующий ход.

Xvalues

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ссылки на пересылку (ранее известные как < em> Универсальные ссылки)

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

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

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

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

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

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

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

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

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

person fredoverflow    schedule 18.07.2012
comment
comment
Есть третья причина, по которой семантика перемещения важна: безопасность исключений. Часто, когда операция копирования может вызывать (потому что необходимо выделить ресурсы, а выделение может завершиться сбоем), операция перемещения может быть беспроигрышной (поскольку она может передать владение существующими ресурсами вместо выделения новых). Всегда приятно иметь операции, которые не могут потерпеть неудачу, и это может иметь решающее значение при написании кода, который обеспечивает гарантии исключений. - person Brangdon; 05.08.2012
comment
@Brangdon Если член не является динамическим, его следует скопировать в конструктор перемещения, и эта копия может вызвать исключение. Для обеспечения безопасности исключений каждый элемент должен быть принудительно перемещен с помощью std::move. - person Peregring-lk; 12.05.2014
comment
Я был с вами вплоть до «Универсальных ссылок», но тогда это все слишком абстрактно, чтобы следовать им. Ссылка сворачивается? Идеальная переадресация? Вы хотите сказать, что ссылка rvalue становится универсальной ссылкой, если тип является шаблонным? Я бы хотел, чтобы был способ объяснить это, чтобы я знал, нужно мне это понимать или нет! :) - person Kylotan; 07.11.2014
comment
@Kylotan да. он просто открывает двери к тому, к чему ведет ссылка rvalue, но поскольку они не полностью относятся к семантике перемещения, он кратко описывает эти концепции. Это не проблема, просто универсальные ссылки google и, естественно, идеальная пересылка будет извлечена из материала, полученного в результате первого поиска. - person v.oddou; 09.03.2015
comment
Пожалуйста, напишите книгу сейчас ... этот ответ дал мне повод полагать, что если вы освещаете другие уголки C ++ таким ясным образом, тысячи других людей поймут это. - person halivingston; 28.09.2015
comment
@halivingston Большое спасибо за ваш отзыв, я очень ценю его. Проблема с написанием книги в том, что это гораздо больше работы, чем вы можете себе представить. Если вы хотите углубиться в C ++ 11 и не только, я предлагаю вам купить Effective Modern C ++ Скотта Мейерса. - person fredoverflow; 28.09.2015
comment
Эй, Фред, угадай что? система значений l / r переформулирована / переработана в C ++ 17, и есть несколько новых механизмов перемещения, включающих уничтожение исходного кода. - person Yakk - Adam Nevraumont; 28.06.2016
comment
Изучение этого языка похоже на выбор того, чему вы можете научиться, чтобы жить, не зная. Я не могу знать ВСЕ. - person Zebrafish; 02.01.2017
comment
Привет, мир - это rvalue или lvalue (в разделе неявное преобразование)? - person stephematician; 14.03.2017
comment
@stephematician Сама строка является rзначением, вы не можете получить ее адрес. - person shayan; 24.05.2017
comment
@fredoverflow hello world, отправляемый в some_function (), является rvalue. и спасибо за отличный ответ. - person shayan; 25.05.2017
comment
@shayan - как я и думал. Надеюсь, кто-нибудь сможет отредактировать ответ, похоже, это опечатка :( - person stephematician; 25.05.2017
comment
@stephematician В expr.prim.general в стандарте C ++ четко указано, что A string literal is an lvalue; all other literals are prvalues. - person fredoverflow; 25.05.2017
comment
@shayan Конечно, вы можете взять его адрес, const char (*p)[12] = &"hello world"; - это совершенно правильный C ++. - person fredoverflow; 25.05.2017
comment
Может быть, стоит добавить аннотацию, которая std::move изменит его аргумент - person Liu Hao; 13.06.2017
comment
@liuhao Не уверен, что вы имеете в виду под словом "изменить" ... У оператора std::move(some_variable); нет побочных эффектов. - person fredoverflow; 14.06.2017
comment
эээ ... Я увеличиваю ограничитель перемещения и std :: move - person Liu Hao; 15.06.2017
comment
Ограничение на чтение из rvalues ​​не очень полезно. Разве это не так же полезно, как ограничение чтения из lvalue? Это просто не добавляет ничего полезного. Нет причин принимать параметр как const T&&, потому что тогда можно было бы принять параметр как const T&, или я что-то пропущу? - person 463035818_is_not_a_number; 13.09.2018
comment
Я прочитал все это, и это имеет большой смысл, но я думаю, что использование умных указателей слишком много добавляет в бейсбол, что делает ответ недоступным для большинства, кто выиграет больше всего. Другими словами ... Поздравляю, вы создали статью в Википедии, имеющую отношение к людям, которые уже понимают большую часть темы. - person Chuck Wolber; 07.12.2019
comment
Отличная ссылка (каламбур). Я заметил, что вы изящно объясняете, почему с семантикой перемещения нормально иметь простой возврат прототипа функции по значению, особенно с идиомой копирования и обмена. Однако вы не совсем понимаете (это тривиально), почему бесполезно писать оба прототипа void f (const A &) и void f (A && a). Более того, было бы неплохо добавить небольшой фрагмент, показывающий, как элегантно написать функцию void f (A &) и void f (A &&) перегрузки без дублирования кода, поскольку std :: move будет стоить static_cast. - person R. Absil; 19.08.2020
comment
@ R.Absil Перегрузки для const A& и A&& могут иметь смысл в некоторых критических для производительности сценариях, чтобы избежать дешевой (но не нулевой) конструкции перемещения. static_cast внутри std::move ничего не стоит, он генерирует нулевой код. - person fredoverflow; 19.08.2020
comment
1. ок. 2. Вы имеете в виду, что не усложняйте, просто вызовите f (std :: move (a))? - person R. Absil; 19.08.2020
comment
Почему char d[64]; не выигрывает от семантики перемещения? Должен ли он быть выделен new, чтобы его можно было перемещать? В комментарии написано // moving a char array means copying a char array, но почему вы не можете вместо этого скопировать указатель на массив? - person Åsmund; 11.11.2020
comment
@ Åsmund Где бы вы хранили указатель? В class cannot_benefit_from_move_semantics нет указателя. Элемент данных char d[64]; - это массив. - person fredoverflow; 11.11.2020
comment
@ Åsmund @fredoverflow Я подумал то же самое, а затем понял, что мы, вероятно, путали массивы, выделенные стеком и кучей. char d[] действительно то же самое, что char* d. Затем вы можете сделать d = new char[64]. Вы можете легко переместить временные данные, выполнив std::swap(d, temporary). Но вы не можете сделать это с выделенным стеком массивом char d[64]; d=temp;, это приведет к ошибке. Это потому, что d - это просто константа с точки зрения компилятора. И это имеет смысл, потому что как вы можете гарантировать, что размер массива останется прежним, если вы можете изменить адрес массива? - person Jonas Daverio; 31.01.2021
comment
@JonasDaverio Внутри структуры char d[] - это гибкий член массива, который является функцией C99, никогда не принятой в стандартный C ++. Гибкие элементы массива не указатели! Единственное место, где char* d и char d[]char d[64]!) являются абсолютно одинаковыми, - это внутри списков параметров функций, потому что компилятор подстраивает параметры массива к параметрам указателя. - person fredoverflow; 31.01.2021
comment
@fredoverflow Это интересно. Вы сказали, что они никогда не были приняты, так какова реальная ситуация с char d [] в C ++ внутри структуры? В чем разница между фактическим указателем и предостережениями в отношении того, чтобы думать о нем как о указателе в целом? - person Jonas Daverio; 31.01.2021
comment
@JonasDaverio Я предлагаю прочитать stackoverflow.com/questions/3047530 Код C ++, соответствующий стандартам, просто не может использовать гибкие элементы массива, g++ -pedantic говорит : ISO C++ forbids flexible array member. Член char d[] не является указателем в том же смысле, что член char d[64] не является указателем: d хранит данные массива напрямую, не указатель на другое место. - person fredoverflow; 31.01.2021
comment
@fredoverflow спасибо за помощь. Думаю, теперь я понял, что это означает, но на самом деле я не понимаю, почему такой массив, как char d[64], не может выиграть от перемещения. Я знаю, что в C ++ нет указателя, но компилятор внутренне представляет переменные по их адресу, так что не могли бы вы просто украсть то место, куда вы помещаете массив при перемещении объекта? - person Jonas Daverio; 31.01.2021
comment
@JonasDaverio Что вы предлагаете написать внутри оператора присваивания перемещения struct X { char d[64]; };? И источник, и место назначения состоят из 64 символов. Здесь нет указателей. Наш единственный выбор - скопировать 64 символа, потому что что еще мы могли бы сделать вместо этого? Во время выполнения struct X { char d[64]; }; выложен точно так же, как struct Y { char d0; char d1; char d2; ... char d63; }. Это делает его более очевидным? - person fredoverflow; 31.01.2021
comment
@fredoverflow на самом деле, у меня тоже проблемы, чтобы понять, почему вы не можете перемещать символы. То, что это было бы невыгодно, понятно, поскольку адрес char занимает больше места, чем char, но с массивом не может ли компилятор делать то же самое, что и при перемещении другого объекта? В конце концов, вы ничего не сообщаете компилятору, кроме того, что это значение xvalue, и вы можете переместить его, если хотите. - person Jonas Daverio; 01.02.2021
comment
@JonasDaverio Учитывая X a; и X b;, что может a = std::move(b); означать помимо копирования 64 символов? Вы не можете динамически изменять статический адрес переменной, если это то, что вы подразумевали. &a == &a.d и &b == &b.d всегда будут держаться. Вы можете эффективно перемещать только косвенные данные (массив с указателем), но не прямые данные (встроенный массив). - person fredoverflow; 01.02.2021
comment
О, я вижу. Так являются ли возвращаемое значение и аргументы функции косвенными данными? Почему прямые данные не подлежат перемещению? Насколько я понимаю, по сравнению со сборкой x86 прямые данные просто хранятся в разделе .data, а косвенные данные хранятся в стеке или в куче. Вероятно, можно было бы обобщить, как это работает на другой платформе, но я их не знаю. Чем отличаются переменные в разделе .data и переменные в куче или стеке? - person Jonas Daverio; 01.02.2021
comment
@JonasDaverio Примером косвенных данных может быть struct Y { char* p; };, где p выделяется из кучи. Тогда неглубокий Y ход (a.p = b.p; b.p = nullptr;) быстрее, чем глубокая Y копия (memcpy(a.p, b.p, 64);). Проблема с struct X { char d[64]; }; в том, что он полностью плоский; мелкие и глубокие просто не применимы, поэтому memcpy(&a.d, &b.d, 64); - единственно возможная реализация (как для копирования, так и для перемещения). - person fredoverflow; 01.02.2021
comment
@JonasDaverio Обратите внимание, что макет объекта не имеет ничего общего с возвращаемыми значениями, аргументами функции или .data / stack / heap. Является ли объект полностью автономным / плоским / неглубоким в памяти? Тогда движение не может быть быстрее копирования. Содержит ли объект внешний ресурс через указатель, дескриптор или что-то подобное? Тогда двигаться можно быстрее, чем копировать. Такое чувство, что у меня закончились аргументы, и я просто повторяюсь. На этом этапе я настоятельно рекомендую вам реализовать семантику перемещения для вашего собственного простого типа контейнера (например, std::string или std::vector). Полностью разобраться в семантике хода простым чтением прозы невозможно. - person fredoverflow; 01.02.2021
comment
@fredoverflow Имейте в виду, я не пытаюсь доказать, что вы ошибаетесь, я просто пытаюсь понять. Я понимаю, что конструктор перемещения совершенно бесполезен, если у вас есть только плоский объект, как вы описываете. Я пытаюсь понять с точки зрения машины. Если в C ++ вы пишете char a[64] = "blabla..."; char b[64] = a;. Допустим, вы можете * гарантировать, что можете выбросить a, иначе вы знаете, что это значение x. Что мешает компилятору просто украсть место, где вы поместили a для хранения данных b? - person Jonas Daverio; 01.02.2021
comment
@fredoverflow Чтобы сделать мою мысль короче, цитирую вас. Вы не можете динамически изменять статический адрес переменной, если это то, что вы имели в виду. Я имею в виду именно это. Теперь я знаю, что это невозможно в C ++, потому что я не понимаю, почему это не реализовано, и, для сравнения, почему это невозможно с точки зрения машины. - person Jonas Daverio; 01.02.2021
comment
@JonasDaverio Повторное использование адресов переменных - это не то, о чем семантика перемещения. Я не удивлюсь, если компиляторы C ++ поддержат такую ​​оптимизацию, но я не являюсь разработчиком компилятора C ++ и не членом комитета по стандартизации. - person fredoverflow; 02.02.2021
comment
@fredoverflow Хорошо, теперь все ясно. Большое спасибо за то, что помогли мне. - person Jonas Daverio; 02.02.2021
comment
@fredoverflow Я читаю и перечитываю ваш ответ на некоторые ссылки примерно через 3-4 недели. Всегда получаю новые идеи. ваш ответ и работа потрясающие. Спасибо от всего сердца. - person Abhishek Mane; 03.06.2021
comment
@fredoverflow A a1=make_A(); это не вызвало конструктор Move, но реализовано нормально. Если я закомментировал определение конструктора Move, он выдает ошибку и A a1=std::move(make_A()); фактически вызывает конструктор Move. вы упомянули std :: move только относительно lvalues, чтобы преобразовать в rvalues точно xvalue - person Abhishek Mane; 04.06.2021
comment
@AbhishekMane Любой достойный компилятор C ++ оптимизирует тривиальные ходы / копии, подобные этому. Если вы хотите отключить эту оптимизацию в образовательных целях, скомпилируйте с флагом -fno-elide-constructors - person fredoverflow; 04.06.2021

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

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

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

Matrix r = multiply(a, b);

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

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

person Greg Hewgill    schedule 23.06.2010
comment
Чем отличаются конструкторы перемещения и конструкторы копирования? - person dicroce; 24.06.2010
comment
@dicroce: они различаются по синтаксису, один выглядит как Matrix (const Matrix & src) (конструктор копирования), а другой выглядит как Matrix (Matrix && src) (конструктор перемещения), проверьте мой основной ответ для лучшего примера. - person snk_kid; 24.06.2010
comment
@dicroce: один создает пустой объект, а другой - копию. Если объем данных, хранящихся в объекте, велик, копирование может быть дорогостоящим. Например, std :: vector. - person Billy ONeal; 24.06.2010
comment
Не могли бы вы объяснить, что вы имели в виду, говоря об отказе от временного значения, не уничтожая его. Создает ли семантика перемещения по-прежнему временный объект или мгновенно мгновенно возвращает возвращаемое значение в адрес R? - person unj2; 14.06.2012
comment
@ kunj2aan: Думаю, это зависит от вашего компилятора. Компилятор может создать временный объект внутри функции, а затем переместить его в возвращаемое значение вызывающей стороны. Или он может иметь возможность напрямую построить объект в возвращаемом значении без необходимости использования конструктора перемещения. - person Greg Hewgill; 14.06.2012
comment
Обратите внимание, что объект должен хранить ресурс через удаленный дескриптор, например указатель на кучу для этого класса Matrix. Если класс содержит свои данные напрямую, тогда это зависит от операций перемещения на уровне элемента для экономии времени (или без экономии, если элементы являются встроенными). - person CTMacUser; 02.09.2013
comment
Обратите внимание, что простой тест с msvc11 показывает, что компилятор может напрямую построить объект в возвращаемом значении, без необходимости использовать конструктор перемещения в режиме выпуска. - person Jichao; 10.10.2013
comment
@Jichao: это оптимизация под названием RVO, см. Этот вопрос для получения дополнительной информации о разнице: stackoverflow.com/questions/5031778/ - person Greg Hewgill; 10.10.2013
comment
@GregHewgill A a1=make_A(); это не вызвало конструктор Move, но реализовано нормально. Если я закомментировал определение конструктора Move, он выдает ошибку и A a1=std::move(make_A()); фактически вызывает конструктор Move. fredoverflow упомянул std :: move только относительно lvalues для преобразования в rvalues точно xvalues - person Abhishek Mane; 04.06.2021

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

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

person James McNellis    schedule 23.06.2010

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

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

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

person Dave Abrahams    schedule 08.04.2012

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

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

class A
{
   int i, *p;

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

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

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

class A
{
   int i, *p;

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

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

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

void heavyFunction(HeavyType());

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

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

class A
{
   int i, *p;

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

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

   ~A() { delete p; }
};

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

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

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

class Heavy
{
   bool b_moved;
   // staff

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

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

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

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

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

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

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

other_function(std::move(a));

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

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

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

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

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

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

person Peregring-lk    schedule 18.08.2013

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

person Terry Mahaffey    schedule 23.06.2010

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

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

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

std::vector<foo> get_foos();

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

person snk_kid    schedule 23.06.2010
comment
Я не думаю, что это отличный пример, потому что в этих примерах возвращаемого значения функции оптимизация возвращаемого значения, вероятно, уже устраняет операцию копирования. - person Zan Lynx; 28.06.2010

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

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

person Andreas DM    schedule 25.02.2016

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

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

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

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

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

person Chris B    schedule 18.11.2016
comment
Назначение перемещения позволяет программисту переопределить поведение копирования по умолчанию и вместо этого поменять местами ссылки на объекты, что означает, что копирование вообще отсутствует, а операция подкачки выполняется намного быстрее. - эти утверждения неоднозначны и вводящие в заблуждение. Чтобы поменять местами два объекта x и y, вы не можете просто поменять местами ссылки на объекты; может случиться так, что объекты содержат указатели, которые ссылаются на другие данные, и эти указатели можно поменять местами, но операторы перемещения не требуются для замены чего-либо. Они могут стереть данные из перемещенного объекта, а не сохранить в нем целевые данные. - person Tony Delroy; 03.05.2020
comment
Вы можете написать swap() без семантики перемещения. Назначение перемещения может быть вызвано вызовом метода std :: move (). - иногда необходимо использовать std::move() - хотя на самом деле это ничего не перемещает - просто позволяет компилятор знает, что аргумент может быть перемещен, иногда std::forward<>() (со ссылками на пересылку), а иногда компилятор знает, что значение может быть перемещено. - person Tony Delroy; 03.05.2020

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

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

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

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

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

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

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

Теперь выписка return res; не копируется!

person Rob Pei    schedule 25.04.2020