Перемещающий конструктор и семантика перемещения

      Комментарии к записи Перемещающий конструктор и семантика перемещения отключены

Главная Форумы Программирование Программирование на С++ Учебные материалы по С++ Перемещающий конструктор и семантика перемещения

В этой теме 1 ответ, 2 участника, последнее обновление  Васильев Владимир Сергеевич 1 год, 2 мес. назад.

  • Автор
    Сообщения
  • #2924

    questioner
    Участник

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

  • #2925

    Перемещающий конструктор. Примеры

    Рассмотрим функцию, выполняющую переворот строки, учитывая, что на вход подано rvalue (временный объект):

    string reverse(string&& str) {
      string reverse_string(str.buffer);
      str.buffer = nullptr;
      std::reverse(reverse_string.begin(), reverse_string.end());
      return reverse_string;
    }

    Тут экономия (более высокая эффективность) обеспечивается за счет того, что результирующая строка использует буфер (какие-то сырые данные, возможно char*) из временной строки. Более подробно про то, как работает этот код: ссылки в С++: rvalue- и lvalue- references. Проблема тут заключается в нарушении инкапсуляции, т.к. сторонний код (функция реверса строки) не должна иметь доступ к сырым данным строки (и даже знать как она устроена внутри).

    Решением проблемы являются перемещающие конструкторы, которые вызываются автоматически вместо конструкторов копирования если аргументом является временный объект. Для сравнения рассмотрим конструктор копирования строки и перемещающий конструктор:

    string(string const& str)
      : m_buf(new char[str.size()+1]), m_size(str.size()) {
      strcpy(m_buf, str.m_buf);
    }
    
    string(string const&& str)
      : m_buf(str.m_buf), // char* m_buf - заменяется указатель
        m_size(str.size()) {
      str.m_buf = nullptr;
      str.m_size = 0;
    }

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

    template <class ElementType>
    Array<ElementType>::Array(Array<ElementType>&& array)
      : m_size(array.m_size), m_realSize(array.m_realSize), m_array(array.m_array) {
        array.m_size = 0;
        array.m_realSize = 0;
        array.m_array = nullptr;
    }

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

    void foo(string str);
    void bar(const string& str);
    
    foo(employee.get_name());
    bar(employee.get_office());

    Преимущества перемещающего конструктора

    Было показано, что перемещающий конструктор является облегченной версией конструктора копирования, однако иногда реализуют перемещающий конструктор для классов, копирование в которых запрещено. Ярчайший пример — std::unique_ptr<>, представляющий собой один из умных указателей стандартной библиотеки, реализующий идиому RAII. Суть этого класса заключается в том, что он владеет единственным указателем на некоторый объект (и автоматически разрушает объект в определенных ситуациях). Указатель должен быть единственным, следовательно конструктор копирования не должен быть доступен, однако перемещение для этого класса вполне логично (мы можем передавать владение указателем от одного объекта к другому — поэтому unique_ptr, в частности, может быть использован в качестве возвращаемого значения функции. Другими примерами такого поведения из стандартной библиотеки являются классы std::fstream и std::thread — копирование для них лишено смысла, однако передача владения файлом из одной функции в другую может быть логична.
    Таким образом, семантика перемещения является не только средством повышения эффективности программ, но и позволяет реализовать передачу владения объектом в случаях, когда копирование запрещено (является очень длительной операцией или вообще лишено смысла).

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

    struct Point {
      double x, y;
      Point(const Point& point) 
        : x(point.x), 
          y(point.y) {
      }
    }

    Для этого примера не получится реализовать перемещающий конструктор более эффективно, чем конструктор копирования.

    std::move

    В ряде случаев нужно явно указать компилятору, что объект является временным — сделать это можно с помощью функции std::move, выполняющей приведение типа к rvalue (эквивалентной static_cast<T&&>). Если вам нужно переместить именованный (не временный) объект — используйте std::move, т.к. в противном случае будет вызван конструктор копирования:

    void foo(Type&& obj) { 
      Type copy_obj(obj); // вызов конструктора копирования
      Type move_obj(std::move(obj)); // вызов перемещающего конструктора
    }
    
    foo(Type()); // успешный вызов функции foo
    Type obj;
    foo(obj); // ошибка, функция принимает ссылку на r-значение, но мы передаем l-значение

    Этот пример напоминает, что функции, принимающие указатели на rvalue будут вызваны только для временных объектов. Однако, внутри функции эти объекты уже не являются временными, ведь у них есть имя — поэтому при создании объекта copy_obj будет вызван конструктор копирования. Если нам нужно перемещение — нужно явно указать, что объект является временным с помощью std::move.

    В рассмотренном выше примере данные строки (класс string) хранились в виде массива символов, поэтому вызов m_buf(str.m_buf) работал эффективно — просто заменялся указатель. Однако, если данных хранились бы в векторе, то такой код привел бы к вызову конструктора копирования:

    string(string const&& str)
      : m_buf(str.m_buf) // vector<char> m_buf, вызывается конструктор копирования 
    {
    }

    Решить проблему можно с помощью std::move:
    string(string const&& str)
      : m_buf(std::move(str.m_buf)) // vector<char> m_buf, вызывается перемещающий конструктор 
    {
    }

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

    std::swap

    Функция std::swap предназначена для обмена значений двух объектов. До принятия стандарта С++11 обмен происходил с использованием вспомогательной переменной, что требовало выполнения конструктора копирования и двух операций присваивания:

    template<class T> 
    void swap(T& a, T& b) {
      T c(a); a=b; b=c;
    }

    Сейчас для классов, поддерживающих семантику перемещения эта функция вызывает перемещающий конструктор и два перемещающих оператора присваивания:

    template <class T> void swap(T& a, T& b) {
      T c(std::move(a)); a=std::move(b); b=std::move(c);
    }

    Это работает очень эффективно и может применяться для более простой реализации перемещающего конструктора:

    string(string&& str)
      : string() {
      swap(*this, str);
    }

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

    Перемещающий оператор присваивания и std::swap

    Оператор присваивания является одной из наиболее часто используемых функций, даже std::swap использует его, при этом семантика перемещения может повысить эффективность:

    Array<int> get_values();
    
    Array<int> values;
    values = get_values();

    В данном примере вызов функции создает временный объект, который присваивается объекту values. Если для класса Array не реализована перемещающая версия оператора, то произойдет копирование данных массива.
    template<typename T>
    Array<T>& operator=(Array<T>&& source) {
      if (this != &source) {
        delete m_array;
    
        m_array = source.m_array;
        m_size = source.m_size;
        m_realSize = source.m_realSize;
    
        source.m_array = nullptr;
      }
      return *this;
    }

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

    С помощью функции std::swap этот код может быть упрощен:

    template<typename T>
    Array<T>& operator=(Array<T> source) {
      swap(*this, source);
      return *this;
    }

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

    Вспомогательная литература:

    1. Ссылки в С++: rvalue- и lvalue- references — описываются ссылки на r-значения, появившиеся в С++11 и лежащие в основе семантики перемещения;
    2. Реализация класса расширяющегося одномерного массива — в примерах статьи используется перемещающий конструктор для этого класса;
    3. Идиома RAII. Объекты управления ресурсами в C++ — описывается идиома, лежащая в основе класса unique_ptr из стандартной библиотеки, рассматриваемого в качестве примера класса, поддерживающего семантику перемещения;
    4. Умный указатель unique_ptr — более подробное описание возможностей и особенностей класса unique_ptr.

Для ответа в этой теме необходимо авторизоваться.