Подтвердить что ты не робот

С++ 11 перемещает семантику, делая что-то новое или просто делая семантику яснее?

В основном я пытаюсь понять, является ли концепция "семантики движения" чем-то совершенно новым или просто упрощает реализацию существующего кода? Я всегда заинтересован в сокращении числа раз, когда я вызываю copy/constructors, но я обычно передаю объекты с помощью ссылки (и, возможно, const) и гарантирую, что всегда использую списки инициализации. Имея это в виду (и, посмотрев на весь уродливый синтаксис & &), интересно, стоит ли использовать эти принципы или просто кодировать, как я уже делаю? Что-то новое делается здесь, или это просто "легче" синтаксический сахар для того, что я уже делаю?

4b9b3361

Ответ 1

TL; DR

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

Long Answer: Почему это новые и некоторые, возможно, неочевидные последствия

Перенос семантики - это то, что подразумевается под именем, то есть способ явно объявить инструкции для перемещения объектов, а не копирования. В дополнение к очевидному повышению эффективности, это также предоставляет программисту стандартизованный способ иметь объекты, которые могут перемещаться, но не копироваться. Объекты, которые могут перемещаться и не копироваться, передают очень четкую границу владения ресурсами через стандартную семантику языка. Это было возможно в прошлом, но для этого не было стандартного/унифицированного (или STL-совместимого) способа.

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

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

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

Уголовно-неполный курс краха RAII (для непосвященных)

Я понимаю, что вы не просили пример кода, но здесь очень простой, который может принести пользу будущему читателю, который может быть менее знаком с этой темой или релевантностью методов Move Semantics для RAII. (Если вы это уже понимаете, пропустите оставшуюся часть ответа)

// non-copyable class that manages lifecycle of a resource
// note:  non-virtual destructor--probably not an appropriate candidate
//        for serving as a base class for objects handled polymorphically.
class res_t {
  using handle_t = /* whatever */;
  handle_t* handle;  // Pointer to owned resource
public:
  res_t( const res_t& src ) = delete;            // no copy constructor
  res_t& operator=( const res_t& src ) = delete; // no copy-assignment

  res_t( res_t&& src ) = default;                // Move constructor
  res_t& operator=( res_t&& src ) = default;     // Move-assignment

  res_t();                                       // Default constructor
  ~res_t();                                      // Destructor
};

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

Ответ 2

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

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

Это повышает эффективность очевидными способами. Перемещение, будь то с помощью конструкции swap или move, намного быстрее для многих типов данных, чем для копирования. Вы можете создавать специальные интерфейсы, чтобы указать, когда можно перемещать вещи: но, честно говоря, люди этого не делали. С семантикой перемещения это становится относительно легко сделать. Сравните стоимость перемещения std::vector на его копирование - move берет грубое копирование 3 указателей, в то время как для копирования требуется выделение кучи, копирование каждого элемента в контейнере и создание 3 указателей.

Более того, сравните reserve с поддержкой std::vector с поддержкой передачи только для копирования: предположим, что у вас есть std::vector of std::vector. В С++ 03 это было самоубийством с точки зрения производительности, если вы раньше не знали размеров каждого компонента - в С++ 11, перемещение семантики делает его гладким, как шелк, потому что он больше не повторно копирует суб - vector всякий раз, когда изменяется внешний вектор.

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

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

std::unique_ptr является заменой для std::auto_ptr. Они оба делают примерно одно и то же, но std::auto_ptr обрабатывают копии как ходы. Это сделало std::auto_ptr смехотворно опасным для использования на практике. Между тем, std::unique_ptr просто работает. Он представляет собой уникальную собственность на некоторый ресурс очень хорошо, и передача собственности может происходить легко и плавно.

Вы знаете проблему, по которой вы берете foo* в интерфейсе, а иногда это означает, что "этот интерфейс принимает собственность на объект", а иногда это означает, что "этот интерфейс просто хочет иметь возможность удаленно модифицировать этот объект", и вам нужно вникать в документацию API, а иногда и исходный код, чтобы выяснить, какой?

std::unique_ptr действительно решает эту проблему. Теперь интерфейсы, которые хотят взять onwership, могут принимать std::unique_ptr<foo>, а передача права собственности очевидна как на уровне API, так и в коде, вызывающем интерфейс. std::unique_ptr - это auto_ptr, который просто работает и удаляет небезопасные части, и заменяется семантикой перемещения. И он делает все это с почти идеальной эффективностью.

std::unique_ptr является передаваемым RAII-представлением ресурса, значение которого представлено указателем.

После того, как вы напишете make_unique<T>(Args&&...), если вы не написали действительно низкоуровневый код, вероятно, лучше никогда не называть new напрямую. Перенос семантики в основном сделал new устаревшим.

Другие представления RAII часто не копируются. Порт, сеанс печати, взаимодействие с физическим устройством - все это ресурсы, для которых "копия" не имеет большого смысла. Большинство из них могут быть легко модифицированы для поддержки семантики перемещения, что открывает целый ряд свобод в решении этих переменных.

Перемещение семантики также позволяет вам поместить возвращаемые значения в возвращаемую часть функции. Образец принятия возвращаемых значений по ссылке (и документирование "этот один не доступен, это один из входов/выходов" или не выполняется) может быть несколько заменен возвратом ваших данных.

Итак, вместо void fill_vec( std::vector<foo>& ) у вас есть std::vector<foo> get_vec(). Это даже работает с несколькими возвращаемыми значениями - std::tuple< std::vector<A>, std::set<B>, bool > get_stuff() может быть вызвано, и вы можете эффективно загружать свои данные в локальные переменные через std::tie( my_vec, my_set, my_bool ) = get_stuff().

Выходные параметры могут быть семантически только выходными, с очень небольшими накладными расходами (выше, в худшем случае, стоит 8 указателей и 2 bool копий, независимо от того, сколько данных у нас есть в этих контейнерах - и что накладные расходы может быть всего лишь 0 указателем и 0 bool копиями с немного большей работой), из-за семантики перемещения.

Ответ 3

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

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

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

Ответ 4

Никакой ответ не будет завершен без ссылки на Томаса Беккера, кропотливо исчерпывающую запись на rvalue-ссылках, совершенную пересылку, обращение к ссылке и все, что связано с этим.

см. здесь: http://thbecker.net/articles/rvalue_references/section_01.html

Ответ 5

Я бы сказал, что да, потому что оператор Move Constructor и Move Assignment теперь компилятор определен для объектов, которые не определяют/не защищают деструктор, конструктор копирования или назначение копии.

Это означает, что если у вас есть следующий код...

struct intContainer
{
    std::vector<int> v;
}

intContainer CreateContainer()
{
    intContainer c;
    c.v.push_back(3);
    return c;
}

Этот код был бы оптимизирован просто путем перекомпиляции с компилятором, который поддерживает семантику перемещения. В вашем контейнере c будет определен механизм семантики перемещения и, таким образом, вызовет вручную определенные операции перемещения для std::vector без каких-либо изменений в вашем коде.

Ответ 6

Так как семантика перемещения применяется только при наличии rvalue ссылки, объявленные новым токеном, &&, похоже очень ясно, что они что-то новое.

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

Что касается 1, мы можем со временем привести к вездесущей эвристический подход к их использованию: ведь, передавая аргумент по ссылке const, а не по значению, также является оптимизация, но вездесущее соглашение - пройти класс типы по константной ссылке и все остальные типы по значению.

Что касается 2, то компиляторов пока нет. По крайней мере обычные. Основные принципы, которые могут быть использованы для Переместить семантику неактуальны (хорошо?) известны, но на сегодняшний день они как правило, приводят к неприемлемым временам компиляции для реальных программ.

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