Pull to refresh

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

Reading time 2 min
Views 77K
Эта небольшая заметка о том, как с приходом нового стандарта 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") в конец массива


Надеюсь, что данная заметка наглядно показывает, насколько важным нововведением является семантика перемещения объектов. Всем успехов в переходе на новый стандарт!
Tags:
Hubs:
+64
Comments 30
Comments Comments 30

Articles