При определении конструкторов перемещения и операторов присвоения с перемещением рекомендуется объявлять их с оператором noexcept, если функции конструктора и оператора присваивания в принципе не генерируют исключение. Сначала посмотрим, зачем это нужно
Тип std::vector представляет динамический список и для добавления объекта определяет функцию push_back(). Эта функция имеет две версии:
void push_back(const Message &_Val) void push_back(Message &&_Val)
То есть если мы передаем в функцию rvalue, срабатывает вторая версия, которая для сохранения данных внутри вектора использует конструктор перемещения. Но посмотрим, что будет, если мы попробуем добавить в вектор несколько объектов:
#include <iostream> #include <vector> // класс сообщения class Message { public: // обычный конструктор Message(const char* data, unsigned count) { size = count; text = new char[size]; // выделяем память for(unsigned i{}; i < size; i++) // копируем данные { text[i] = data[i]; } id = ++counter; std::cout << "Create Message " << id << std::endl; } // конструктор копирования Message(const Message& copy) : Message{copy.getText(), copy.size } // обращаемся к стандартному конструктору { std::cout << "Copy Message " << copy.id << " to " << id << std::endl; } Message(Message&& moved) { id = ++counter; std::cout << "Create Message " << id << std::endl; text = moved.text; // перемещаем текст сообщения size = moved.size; // копируем размер сообщения moved.text = nullptr; std::cout << "Move Message " << moved.id << " to " << id << std::endl; } // деструктор ~Message() { std::cout << "Delete Message " << id << std::endl; delete[] text; // освобождаем память } char* getText() const { return text; } unsigned getSize() const { return size; } unsigned getId() const {return id;} private: char* text{}; // текст сообщения unsigned size{}; // размер сообщения unsigned id{}; // номер сообщения static inline unsigned counter{}; // статический счетчик для генерации номера объекта }; int main() { std::vector<Message> messages{}; messages.push_back(Message{"Hello world", 12}); messages.push_back(Message{"Bye world", 10}); }
Здесь определен класс условного сообщения Message. Текст сообщения хранится в динамической памяти и доступен через указатель text. Для большей наглядности используем обычные указатели, а не smart-указатели. Также, чтобы был виден весь процесс создания/копирования/удаления данных в классе сообщения определена статическая переменная counter, которая будет увеличиваться с созданием каждого нового объекта. И текущее значение счетчика будет присваиваться переменной id, которая представляет номер сообщения:
char* text{}; // текст сообщения unsigned size{}; // размер сообщения unsigned id{}; // номер сообщения static inline unsigned counter{}; // статический счетчик для генерации номера объекта
В конструкторе Message выделяем динамическую память для символов сообщения, устанавливаем размер и номер сообщения, а в деструкторе освобождаем память. Для копирования данных в Message определен конструктор копирования.
Также Message определяет конструктор перемещения:
Message(Message&& moved) { id = ++counter; std::cout << "Create Message " << id << std::endl; text = moved.text; // перемещаем текст сообщения size = moved.size; // копируем размер сообщения moved.text = nullptr; std::cout << "Move Message " << moved.id << " to " << id << std::endl; }
Здесь мы не вызваем стандартный конструктор, как в случае с конструктором копирования, потому что нам не надо выделять память.
Вместо этого мы просто передаем в переменную text значение указателя (адрес блока выделенной памяти) из перемещаемого объекта moved text{moved.text}
. И чтобы указатель text перемещаемого объекта moved перестал указывать на эту область памяти, и соответственно чтобы в деструкторе
объекта moved не было ее освобождения, передаем указателю значение nullptr
.
В функции main в вектор добавляется два объекта Message, которые представляют rvalue:
std::vector<Message> messages{}; messages.push_back(Message{"Hello world", 12}); messages.push_back(Message{"Bye world", 10});
Посмотрим, каким будет консольный вывод:
Create Message 1 Create Message 2 Move Message 1 to 2 Delete Message 1 Create Message 3 Create Message 4 Move Message 3 to 4 Create Message 5 Copy Message 2 to 5 Delete Message 2 Delete Message 3 Delete Message 5 Delete Message 4
Итак, мы добавляем в вектор 2 объекта Message, но у нас в итоге создается 5 объектов Message. Рассмотрим по этапно. Вначале добавляем один объект Message:
messages.push_back(Message{"Hello world", 12});
В итоге создается один объект Message, который представляет rvalue. Из него с помощью конструктора перемещения данные перемещаются в другой объект Message, который хранится внутри вектора.
Create Message 1 Create Message 2 Move Message 1 to 2 Delete Message 1
Вектор - динамический список, в который мы можем добавлять значения, но эта динамика в своей внутренней реализации при добавлении новых элементов выделяет новый участок динамической памяти, чтобы вместить уже имеющиеся элементы и новые добавляемые элементы. Это приводит к тому, что вектор копирует данные из старого участка памяти в новые. В итоге при выполнении строки
messages.push_back(Message{"Bye world", 10});
Выделяется память для двух объектов Message. Опять же создается rvalue-объект, его данные перемещаются в объект Message внутри вектора. Но вместе с этим из ранее выделенного участка памяти первый добавленный объект копируется в новый участок памяти. Причем при копировании применяется конструктор копирования. Что мы видим по консольному выводу
Create Message 3 Create Message 4 Move Message 3 to 4 Create Message 5 Copy Message 2 to 5 Delete Message 2 Delete Message 3
Почему здесь используется конструктор копирования, а не конструктор перемещения, который в данном случае был бы более предпочтителен? Дело в том, что вектор не уверен, что конструктор перемещения не сгенерирует исключение и в этом случае прибегает к конструктору копирования.
Но в данном случае у нас нет в конструкторе перемещения каких-то моментов, которые могли бы привести к генерации исключения. Поэтому определим конструктор с ключевым словом noexcept:
Message(Message&& moved) noexcept { id = ++counter; std::cout << "Create Message " << id << std::endl; text = moved.text; // перемещаем текст сообщения size = moved.size; // копируем размер сообщения moved.text = nullptr; // обнуляем указатель перемещенного объекта std::cout << "Move Message " << moved.id << " to " << id << std::endl; }
И если теперь мы перекомпилируем и запустим программу, то мы увидим, что вместо конструктора копирования будет применяться конструктор перемещения:
Create Message 1 Create Message 2 Move Message 1 to 2 Delete Message 1 Create Message 3 Create Message 4 Move Message 3 to 4 Create Message 5 Move Message 2 to 5 Delete Message 2 Delete Message 3 Delete Message 5 Delete Message 4
Данный механизм копирования применяется не только типом вектор, поэтому конструктор перемещения и оператор присваивания с перемещением следует определять с ключевым словом noexcept.