Эта небольшая заметка о том, как с приходом нового стандарта 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") в конец массива
Надеюсь, что данная заметка наглядно показывает, насколько важным нововведением является семантика перемещения объектов. Всем успехов в переходе на новый стандарт!
Автор: naething