Вдохновленный ответом Говарда, я написал статью на эту тему, надеюсь, что это может помочь кому-то, кто тоже интересуется этим. Я копирую / вставляю статью сюда.
Когда я изучал семантику перемещения, у меня всегда было чувство, что, хотя я достаточно хорошо знал эту концепцию, я не могу вписать ее в общую картину C ++. Семантика перемещения не похожа на синтаксический сахар, который существует исключительно для удобства, она глубоко повлияла на то, как люди думают и пишут на C ++, и стала одной из самых важных идиом C ++. Но эй, пруд C ++ уже был полон других идиом, когда вы добавляете семантику перемещения, вместе с этим происходит взаимное выдавливание. Перемещала ли семантика, улучшала или заменяла другие идиомы? Я не знаю, но я хочу узнать.
Семантика значений
Семантика значений - это то, что заставляет меня задуматься над этой проблемой. Поскольку в C ++ не так много вещей с названием «семантика», я, естественно, подумал: «может быть, семантика value и move имеет какие-то связи?». Оказывается, это не просто связи, это источник:
Семантика перемещения не является попыткой заменить семантику копирования и никоим образом не подорвать ее. Скорее это предложение стремится расширить семантику копирования.
- Предложение по семантике Move , 10 сентября 2002
Возможно, вы заметили, что он использует формулировку "семантика копирования", фактически "семантика значения" и "семантика копирования" - это одно и то же, и я буду использовать их взаимозаменяемо.
ОК, так что такое семантика значений? Об этом говорит isocpp вся страница , но, по сути, семантика значения означает, что присваивание копирует значение , например T b = a;
. Это определение, но часто семантика значения просто означает создание, использование, сохранение самого объекта, передачу, возврат по значению, а не указатели или ссылки .
Противоположная концепция - ссылочная семантика, где присваивание копирует указатель. В семантике ссылок важна идентичность, например, T& b = a;
, мы должны помнить, что b
- это псевдоним a
, а не что-либо еще. Но в семантике значений мы вообще не заботимся об идентичности, мы заботимся только о том, какое значение имеет объект 1 . Это связано с характером копирования, поскольку гарантируется, что копия дает нам два независимых объекта, которые имеют одинаковое значение, вы не можете сказать, какой из них является источником, и это не влияет на использование.
В отличие от других языков (Java, C #, JavaScript), C ++ построен на семантике значений. По умолчанию, присваивание выполняется побитовым копированием (если не задан пользовательский ctor копирования), аргументы и возвращаемые значения создаются путем копирования (да, я знаю, что есть RVO). Сохранение значения семантики считается хорошей вещью в C ++. С одной стороны, это безопаснее, потому что вам не нужно беспокоиться о свисающих указателях и всяких жутких вещах; с другой стороны, это быстрее, потому что у вас меньше косвенного обращения, см. здесь для официального объяснения.
Семантика перемещения: двигатель V8 на автомобиле семантики значения
Семантика перемещения не является попыткой вытеснить семантику копирования. Они полностью совместимы друг с другом. Я придумал эту метафору, которая, как мне кажется, очень хорошо описывает их отношение.
Представьте, что у вас есть машина, она ровно работала со встроенным двигателем. Однажды вы установили дополнительный двигатель V8 на эту машину. Всякий раз, когда у вас есть достаточно топлива, двигатель V8 способен ускорить ваш автомобиль, и это вас радует.
Итак, автомобиль - это семантика значения, а двигатель V8 - это семантика движения. Установка двигателя на вашем автомобиле не требует нового автомобиля, это все тот же автомобиль, точно так же, как использование семантики перемещения не заставит вас потерять семантику значений, потому что вы все еще работаете с самим объектом, а не с его ссылками или указателями. Более того, ход , если вы можете, иначе скопируйте стратегию , реализованную настройками привязки , точно такой же, как и выбран движок, то есть использовать V8. если можете (достаточно топлива), иначе вернитесь к оригинальному двигателю.
Теперь у нас есть довольно хорошее понимание того, как Говард Хиннант (основной автор предложения о переезде) ответ на SO:
Семантика перемещения позволяет нам сохранить семантику значений, но в то же время повысить производительность эталонной семантики в тех случаях, когда значение исходного (скопированного) объекта не имеет значения для логики программы.
РЕДАКТИРОВАТЬ : Говард добавил несколько комментариев, которые действительно стоит упомянуть. По определению семантика перемещения действует больше как семантика ссылок, потому что объекты Move-to и Move-from не являются независимыми, при изменении (либо путем перемещения-конструирования, либо перемещения-присваивания) объекта Move-to объект Move-from является также модифицировано. Тем не менее, это на самом деле не имеет значения - когда имеет место семантика перемещения, вы не заботитесь о перемещенном объекте , это либо чистое значение (так что никто больше не имеет ссылки на оригинал) или когда программист специально говорит: «Меня не волнует значение оригинала после копии» (используя std::move
вместо копии). Поскольку изменение исходного объекта не влияет на программу, вы можете использовать перемещенный объект, как если бы он был независимой копией, сохраняя при этом внешний вид семантики значений.
Семантика перемещения и оптимизация производительности
Семантика перемещения в основном связана с оптимизацией производительности: возможность перемещать дорогой объект с одного адреса в памяти на другой, при этом воровать ресурсы источника для построения цели с минимальными затратами.
- Предложение по семантике Move
Как указано в предложении, главное преимущество, которое люди получают от семантики перемещения, - это повышение производительности. Я приведу здесь два примера.
Оптимизация, которую вы можете увидеть
Предположим, у нас есть обработчик (какой бы он ни был), который дорого построить, и мы хотим сохранить его на карте для будущего использования.
std::unordered_map<string, Handler> handlers;
void RegisterHandler(const string& name, Handler handler) {
handlers[name] = std::move(handler);
}
RegisterHandler("handler-A", build_handler());
Это типичное использование перемещения, и, конечно, предполагается, что Handler
имеет ctor хода. Перемещая (не копируя) конструирующее значение карты, можно сэкономить много времени.
Оптимизация, которую вы не видите
Говард Хиннант однажды упомянул в своем выступлении , что идея семантики перемещения возникла из оптимизации std::vector
. Как?
Объект std::vector<T>
- это, по сути, набор указателей на внутренний буфер данных в куче, например begin()
и end()
. Копирование вектора стоит дорого из-за выделения новой памяти для буфера данных. Когда вместо копирования используется перемещение, копируются только указатели, указывающие на старый буфер.
Более того, перемещение также усиливает векторную insert
операцию. Это объясняется в разделе vector Example в предложении. Допустим, у нас есть std::vector<string>
с двумя элементами "AAAAA"
и "BBBBB"
, теперь мы хотим вставить "CCCCC"
по индексу 1. Предполагая, что вектор имеет достаточную емкость, на следующем графике показан процесс вставки с копией против перемещения.
Все, что показано на графике, находится в куче, включая буфер данных вектора и буфер данных каждой строки элемента. При копировании необходимо скопировать буфер данных str_b
, который включает выделение буфера, а затем освобождение. При перемещении старый буфер данных str_b
повторно используется новым str_b
в новом адресе, выделение или освобождение буфера не требуется (как указал Говард, «данные», на которые теперь указывает старый str_b
, неопределенные). Это дает огромный прирост производительности, но это означает нечто большее, потому что теперь вы можете хранить дорогие объекты в векторе, не жертвуя при этом производительностью, в то время как ранее нужно было хранить указатели. Это также помогает расширить использование семантики значений.
Перемещение семантики и управление ресурсами
В известной статье Правило нуля автор написал:
Использование семантики значений важно для RAII, поскольку ссылки не влияют на время жизни своих ссылок.
Я считаю, что это хорошая отправная точка для обсуждения корреляции между семантикой перемещения и управлением ресурсами.
Как вы можете знать или не знать, RAII имеет другое имя, называемое Управление ресурсами, связанными с областью действия (SBRM), после базового варианта использования, когда время жизни объекта RAII заканчивается из-за выхода из области действия. Помните одно преимущество использования семантики значения? Безопасность. Мы точно знаем, когда время жизни объекта начинается и заканчивается, просто взглянув на его продолжительность хранения , и в 99% случаев мы найдем его в области видимости блока, что делает его очень простым. Все становится намного сложнее для указателей и ссылок, теперь нам нужно беспокоиться о том, был ли освобожден объект, на который ссылаются или на который указывают. Это сложно, и что еще хуже, это то, что эти объекты, как правило, существуют в разных областях, чем указатели и ссылки.
Очевидно, что семантика значений хорошо сочетается с RAII - RAII связывает жизненный цикл ресурса с временем жизни объекта, а с семантикой значений у вас есть четкое представление о времени жизни объекта.
Но ресурс - это личность ...
Хотя семантика значений и RAII кажутся идеальными, на самом деле это не так. Зачем? В сущности говоря, потому что ресурс - это идентичность, а семантика значения - только ценность. У вас есть открытая розетка, вы используете самую розетку; у вас есть открытый файл, вы используете тот же файл. В контексте управления ресурсами нет вещей с одинаковым значением. Ресурс представляет себя с уникальной идентичностью.
Видите противоречие здесь? До C ++ 11, если мы придерживались семантики значений, было трудно работать с ресурсами, потому что они не могут быть скопированы, поэтому программисты придумали некоторые обходные пути:
- Используйте сырые указатели;
- Напишите свой собственный перемещаемый, но не копируемый класс (часто включает частное копирование ctor и такие операции, как
swap
и splice
);
- Использовать
auto_ptr
.
Эти решения предназначены для решения проблемы уникальной собственности и передачи собственности, но все они имеют некоторые недостатки. Я не буду говорить об этом здесь, потому что это везде в Интернете. Я хотел бы обратить внимание на то, что даже без семантики перемещения можно осуществлять управление владением ресурсами, просто оно требует больше кода и часто подвержено ошибкам.
Чего не хватает, так это единого синтаксиса и семантики, позволяющих универсальному коду перемещать произвольные объекты (так же, как универсальный код сегодня может копировать произвольные объекты).
- Предложение по семантике перемещения
По сравнению с приведенным выше предложением из предложения, мне нравится ответ больше:
В дополнение к очевидному выигрышу в эффективности, это также дает программисту совместимый со стандартами способ иметь объекты, которые являются подвижными, но не копируемыми . Подвижные и не подлежащие копированию объекты передают очень четкую границу владения ресурсами с помощью стандартной языковой семантики ... Моя точка зрения заключается в том, что семантика перемещения теперь является стандартным способом для краткого выражения (среди прочего) подвижных, но не копируемых объектов .
Приведенная выше цитата проделала довольно хорошую работу, объясняя, что означает семантика перемещения для управления владением ресурсами в C ++. Естественно, ресурс должен быть подвижным (я имею в виду «подвижный», я имею в виду переносимый), но не копируемым, теперь с помощью семантики перемещения (ну, на самом деле, для поддержки этого достаточно изменений на уровне языка), есть стандартный способ сделать это правильно и эффективно.
Семантика возрождения значения
Наконец, мы можем поговорить о другом аспекте (помимо производительности) аугментации, который перемещает семантику, приводящую к семантике значения.
Проходя вышеописанное обсуждение, мы увидели, почему семантика значений соответствует модели RAII, но в то же время не совместима с управлением ресурсами. С появлением семантики перемещения необходимые материалы для заполнения этого пробела наконец-то подготовлены. Вот и мы, умные указатели!
Излишне говорить о важности std::unique_ptr
и std::shared_ptr
, здесь я хотел бы подчеркнуть три вещи:
- Они следуют RAII;
- Они используют огромное преимущество семантики перемещения (особенно для unique_ptr);
- Они помогают сохранить семантику значения.
Что касается третьего пункта, если вы прочитали Правило нуля , вы знаете, о чем я говорю. Не нужно использовать необработанные указатели для управления ресурсами, НИКОГДА, просто используйте unique_ptr напрямую или сохраните как переменную-член, и все готово. При передаче владения ресурсами неявно сконструированный ctor перемещения способен хорошо выполнять свою работу. Более того, текущая спецификация гарантирует, что именованное значение в операторе возврата в худшем случае (т. Е. Без исключений) будет рассматриваться как значение. Это означает, что возврат по значению должен быть выбором по умолчанию для unique_ptr .
std::unique_ptr<ExpensiveResource> foo() {
auto data = std::make_unique<ExpensiveResource>();
return data;
}
std::unique_ptr<ExpensiveResource> p = foo(); // a move at worst
См. здесь для более подробного объяснения. На самом деле, при использовании unique_ptr в качестве параметров функции, передача по значению по-прежнему является лучшим выбором. Я, вероятно, напишу статью об этом, если будет время.
Помимо умных указателей, std::string
и std::vector
также являются обертками RAII, и ресурс, которым они управляют, - это куча памяти. Для этих классов возврат по значению все еще предпочтителен. Я не слишком уверен в других вещах, таких как std::thread
или std::lock_guard
, потому что у меня нет шансов использовать их.
Подводя итог, используя интеллектуальные указатели, семантика значений теперь действительно получает совместимость с RAII. По своей сути это основано на семантике перемещения.
Резюме
Пока мы прошли через множество концепций, и вы, вероятно, чувствуете себя подавленными, но моменты, которые я хочу донести, просты:
- Семантика перемещения повышает производительность, сохраняя семантику значений;
- Семантика перемещения помогает объединить все элементы управления ресурсами, чтобы они стали тем, чем они являются сегодня. В частности, это ключ, который заставляет семантику значений и RAII действительно работать вместе, как это должно было быть давно.
Я сам изучаю эту тему, поэтому не стесняйтесь указывать на все, что вы считаете неправильным, я действительно ценю это.
[1]: Здесь объект означает « фрагмент памяти, который имеет адрес, тип и способен хранить значения », из блога Анджея C ++ .