При определении конструкторов перемещения и операторов присвоения с перемещением рекомендуется объявлять их с оператором 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.