Роль noexcept при перемещении

Последнее обновление: 23.03.2023

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850