Move semantics в C++11 и STL-контейнеры

    Эта небольшая заметка о том, как с приходом нового стандарта C++11 изменились требования стандартных контейнеров к своим элементам. В C++98 от элемента контейнера требовалось, по сути, наличие «разумных» конструктора копирования и оператора присваивания. Если, например, объект вашего класса владеет каким-либо ресурсом, копирование обычно становится невозможным (по крайней мере, без «глубокого» копирования ресурса). В качестве примера давайте рассмотрим следующий класс-обертку вокруг FILE*, написанную на C++98:

    class File
    {
        FILE* handle;
    public:
        File(const char* filename) {
            if ( !(handle = fopen(filename, "r")) )
                throw std::runtime_error("blah blah blah");
        }
        ~File() { if (handle) fclose(handle); }
        // ...
    private:
        File(const File&); //запретить копирование
        void operator=(const File&); //запретить присваивание
    };
    



    Мы запретили копирование и присваивание объектов этого класса, поскольку копирование FILE* потребовало бы некоторых платформо-зависимых ухищрений, и вообще не имеет особого физического смысла.

    Что же делать, если требуется хранить целый список объектов типа File? К сожалению, мы не можем использовать File в стандартном контейнере, то есть такой код просто не скомпилируется:

    std::vector<File> files;
    files.push_back(File("data.txt"));
    


    Типичным решением такой проблемы в C++98 является использование shared_ptr:
    std::vector<boost::shared_ptr<File> > files;
    files.push_back(boost::shared_ptr<File>(new File("data.txt")) );
    


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

    Если мы разрешаем использование C++11, то картина сильно меняется. С появлением move semantics, стандартные контейнеры больше не требуют наличия обычных конструктора копирования и оператора присваивания, если только вы не собираетесь копировать контейнер целиком. Вместо них достаточно наличия семантики перемещения. Давайте посмотрим, как мы можем переписать пример с классом File на C++11:

    class File
    {
        FILE* handle;
    public:
        File(const char* filename) {
            if ( !(handle = fopen(filename, "r")) )
                throw std::runtime_error("blah blah blah");
        }
        ~File() { if (handle) fclose(handle); }
    
        File(File&& that) {
            handle = that.handle;
            that.handle = nullptr;
        }
    
        File& operator=(File&& that) {
            std::swap(handle, that.handle);
            return *this;
        }
    
        File(const File&) = delete; //запретить копирование
        void operator=(const File&) = delete; //запретить присваивание
    
        // ...
    };
    


    Мы снова запрещаем обычное копирование, но разрешаем перемещение объекта. Теперь такой код работает:

    std::vector<File> files;
    files.push_back(File("data1.txt"));
    files.push_back(File("data2.txt"));
    files.erase(files.begin());
    


    Кроме того, благодаря variadic templates, в контейнерах появилась новая шаблонная функция emplace_back, которая позволяет создать объект прямо в контейнере, не копируя его:

    std::vector<File> files;
    files.emplace_back("data1.txt"); // добавить File("data1.txt") в конец массива
    


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

    Комментарии 30

      +9
      Коротко и ясно.
      Большое спасибо!
        –5
        т.е. это как-будто мы написали собственный std::vector с запрещёнными операциями копирования и создания… просто и гениально!
          0
          о, извините, господа, за мою глупую, детскую восхищённость и открытость. Впредь такого не будет.
          +1
          Я вам поставил плюс, но для полноты картины вы забыли конструктор File().
            –1
            that.handle = nullptr;
            Эээ… А разве понятие r-value reference не подразумевает константость that?
              +1
              Не подразумевает. В этом основной смысл: можно «переместить» данные из временного объекта, изменив его значение. А объявлять можно и как «const &&», и как "&&".
                +3
                Насколько я понимаю, если мы не изменим that, ничего не выйдет: в that останется ненулевой handle, который будет закрыт в деструкторе, в результате чего в this окажется указатель на уничтоженный FILE.
                  0
                  Читайте коммент. Смысл в swap. Вы темповый объект меняете местами с инициализируемым.
                +1
                А почему не так?
                File(File&& that) : handle(nullptr) { *this = that; }
                  0
                  Можно и так, почему нет :). Код не претендует на идеальную реализацию, хотел лишь донести идею.
                    +2
                    Кажется, это не должно копилироваться. Именованый объект не передаётся как r-value reference дальше (так он мог бы использоваться дальше), поэтому правильней писать так:
                    File(File&& that) : handle(nullptr) { *this = std::move(that); } 
                    


                    С другой стороны вариант автора — это классическая идиома Copy-and-swap, широко применяемая для написания exception-safe operator=.
                    0
                    После беглого знакомства с C++11 не очень очевидна разница между move semantics и r-value references. Так понимаю что r-value reference это сама возможность сослаться на временный объект, тогда как move semantic — возможность использования ссылки на временный объект в конструкторе или присваивании. Или не так? Может кто нибудь внести ясность?
                      0
                      r-value reference — это новый тип, move semantics — новая идеология. Они сильно связаны, но разница очевидна — это просто разные вещи :)
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • НЛО прилетело и опубликовало эту надпись здесь
                      • НЛО прилетело и опубликовало эту надпись здесь
                          +2
                          Если честно не понял к чему здесь OOP, но вот такой код компилироваться будет:
                          std::vector<File> files;
                          File file("data1.txt");
                          files.push_back(std::move(file));
                          

                          std::move нужен показать, что можно забрать владение у объекта file.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              +1
                              А как будет происходить удаление объектов в вашем примере? Есть ли в Objective C сборщик мусора, или же NSMutableArray позаботится об этом?

                              В C++ тоже можно написать просто вот так:
                              std::vector<File*> files;
                              File* file = new File("file1.txt");
                              files.push_back(file);
                              

                              и никаких вспомогательных сущностей не надо. Вопрос только в том, кто будет эти объекты потом удалять. Можно либо воспользоваться аналогом vector, который делает это автоматически, либо удалять вручную, либо прикрутить к C++ сборщик мусора.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Я согласен с тем, что C++ и не является чисто OOP языком, и мне это нравится :)

                                std::move / std::shared_ptr / std::unique_ptr / RAII используются чтобы определить время жизни ресурса и семантику владения. Это идиомы языка, которая в ООP языках со сборщиком мусора востребованы реже. Тем не менее наличие using в C#, try-with-resources в Java, SoftReference/WeakReference/PhantomReference в Java не делает их не-OOP языками.
                                Не думаю, что OOP язык не имеет права управлять временем жизни объектов.

                                Если взять строчку
                                files.push_back(std::move(file));
                                

                                то аналогом на Objective-C могло бы быть
                                [files addObject:file];
                                [file "doNotReferenceAnymoreTheFileObjectSoOwnershipWillTransferToThFilesObject"];
                                

                                • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    >>В C++ это ещё значит буквально — «создать копию объекта и добавить».
                                    Ну и на других языках тоже не все так просто. К примеру на питоне «Взять ссылку на объект и добавить в коллекцию».

                                    >>files.push_back(std::move(file));
                                    Нет. Я бы вырвал любому руки за скрытие этого факта! Такие вещи должен делать программист сознательно. Программирование это не компьютерная игра — это ответственность за многие решения принятые и принимаемые в процессе разработке.

                                    Мы отлично понимаем, что ситуации разные бывают и именно поэтому Страуструп о многом сказал на стр.37 в своей книге «Дизайн и эволюция C++». Почитайте книгу о дизайне и многое станет кристально чистым и понятным. Другими словами не надо С++ натягивать на те вещи и проблемы, которые лучше решать с помощью других языков!
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      C++ ушел еще со времен 98 стандарта, посмотрите на стандартную библиотеку. Это в 80-е было модно ООП, все дела.
                                      Но вот конкретно в этом случае никаких отклонений нет, просто FILE — объект с value-семантикой.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                          0
                                          Дык я о чем. Термины имеют косвенное отношение к ООП, но прямое — к вашему вопросу, это вопрос этой плоскости. Ответ такой: тип File, как и любой другой дескриптор, должен вести себя как ссылка, а не перемещаться от владельца к владельцу. То есть счетчик ссылок и вперед. Снаружи объект будет выглядеть так, как вы хотите. То, что в Objective-C везде ссылки, не делает его более ООПшным.
                                          При чем тут функциональное программирование? Говорю же — посмотрите на стандартную библиотеку. C++ перерос ООП — оно там есть, но есть куча своих паттернов и способов что-либо сделать.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                files.emplace_back(«data1.txt»);
                                • НЛО прилетело и опубликовало эту надпись здесь

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое