Назад к вопросам
Middle+
103
questionbank

Почему семантика перемещения не будет работать с оператором перемещения, который не помечен как noexcept?

Sobes Copilot

Получайте ответы в реальном времени

Ответ от нейросети

sobes.tech AI

Семантика перемещения в C++ предназначена для эффективного переноса ресурсов из временных объектов. Если оператор перемещения (например, operator= или конструктор перемещения) не помечен как noexcept, компилятор не может безопасно использовать его при автоматическом выборе между копированием и перемещением в некоторых стандартных алгоритмах и контейнерах (например, при изменении размера std::vector).

Причина в следующем:

  1. Гарантии исключений: Стандартные контейнеры и алгоритмы могут предоставлять сильные гарантии исключений (например, "no-throw guarantee" - не генерировать исключений при определенных операциях).
  2. Откат (Rollback): В случае исключения, сгенерированного при перемещении элемента (например, при перемещении элемента в новое место в векторе, которое оказалось недействительным), контейнер должен иметь возможность вернуться в исходное корректное состояние.
  3. Небезопасность отката при исключении из оператора перемещения: Если оператор перемещения генерирует исключение, то исходный объект, из которого осуществлялся перенос ресурсов, может оказаться в неопределенном, частично перемещенном или невалидном состоянии. Откатить такую операцию безопасно, восстановив исходное состояние контейнера, становится невозможно или крайне сложно.

Когда оператор перемещения помечен как noexcept, компилятор знает, что исключение из него не будет сгенерировано. Это позволяет стандартным алгоритмам и контейнерам безопасно использовать перемещение, полагаясь на то, что операция завершится успешно и не нарушит целостность данных в случае, если потребуется откатиться.

Без noexcept, даже если операция перемещения в реальности не генерирует исключений, компилятор не может гарантировать этого и вынужден выбрать копирование (которое обычно предоставляет более сильные гарантии исключений), чтобы обеспечить целостность контейнера в случае потенциального исключения.

Пример:

#include <vector>
#include <iostream>

class Resource {
public:
    Resource() { data = new int[100]; std::cout << "Default ctor\n"; }
    ~Resource() { delete[] data; std::cout << "Dtor\n"; }

    // Копирующий конструктор (для примера, можетthrow)
    Resource(const Resource& other) {
        data = new int[100]; // Может бросить исключение std::bad_alloc
        std::copy(other.data, other.data + 100, data);
        std::cout << "Copy ctor\n";
    }

    // Копирующий оператор присваивания
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data;
            data = new int[100]; // Может бросить исключение std::bad_alloc
            std::copy(other.data, other.data + 100, data);
        }
        std::cout << "Copy assign\n";
        return *this;
    }

    // Конструктор перемещения БЕЗ noexcept
    Resource(Resource&& other) {
        data = other.data;
        other.data = nullptr; // Состояние other после неудачного move будет неопределенным если throw
        std::cout << "Move ctor (no noexcept)\n";
        // Имитация возможного исключения (для демонстрации проблемы)
        // if (true) throw std::runtime_error("Error during move!");
    }

    // Оператор перемещения БЕЗ noexcept
    Resource& operator=(Resource&& other) {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr; // Состояние other после неудачного move будет неопределенным если throw
        }
        std::cout << "Move assign (no noexcept)\n";
        // Имитация возможного исключения
        // if (true) throw std::runtime_error("Error during move!");
        return *this;
    }

private:
    int* data;
};

class ResourceNoexcept {
public:
    ResourceNoexcept() { data = new int[100]; std::cout << "Noexcept Default ctor\n"; }
    ~ResourceNoexcept() { delete[] data; std::cout << "Noexcept Dtor\n"; }

    // Копирующий конструктор
    ResourceNoexcept(const ResourceNoexcept& other) {
        data = new int[100];
        std::copy(other.data, other.data + 100, data);
        std::cout << "Noexcept Copy ctor\n";
    }

    // Копирующий оператор присваивания
    ResourceNoexcept& operator=(const ResourceNoexcept& other) {
        if (this != &other) {
            delete[] data;
            data = new int[100];
            std::copy(other.data, other.data + 100, data);
        }
        std::cout << "Noexcept Copy assign\n";
        return *this;
    }

    // Конструктор перемещения С noexcept
    ResourceNoexcept(ResourceNoexcept&& other) noexcept {
        data = other.data;
        other.data = nullptr; // Безопасно, т.к. не бросает исключений
        std::cout << "Noexcept Move ctor (with noexcept)\n";
    }

    // Оператор перемещения С noexcept
    ResourceNoexcept& operator=(ResourceNoexcept&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr; // Безопасно, т.к. не бросает исключений
        }
        std::cout << "Noexcept Move assign (with noexcept)\n";
        return *this;
    }

private:
    int* data;
};

int main() {
    // Вектор с классом Resource (без noexcept на перемещении)
    std::cout << "--- Vector with Resource (no noexcept) ---\n";
    std::vector<Resource> vec1;
    vec1.reserve(3); // Initial capacity
    vec1.emplace_back();
    vec1.emplace_back();
    // При resize, может произойти копирование вместо перемещения
    // если компилятор не уверен, что перемещение безопасно
    std::cout << "Resizing vec1...\n";
    vec1.resize(3); // Если capacity < 3, произойдет увеличение, может триггернуть переаллокацию
                    // и в этом случае элементы будут КОПИРОВАТЬСЯ!
    std::cout << "-----------------------------------------\n\n";

    // Вектор с классом ResourceNoexcept (с noexcept на перемещении)
    std::cout << "--- Vector with ResourceNoexcept (with noexcept) ---\n";
    std::vector<ResourceNoexcept> vec2;
    vec2.reserve(3); // Initial capacity
    vec2.emplace_back();
    vec2.emplace_back();
     // При resize с noexcept перемещением, произойдет перемещение!
    std::cout << "Resizing vec2...\n";
    vec2.resize(3); // Если capacity < 3, произойдет увеличение, триггернет переаллокацию
                    // и в этом случае элементы будут ПЕРЕМЕЩАТЬСЯ!
    std::cout << "--------------------------------------------\n";


    return 0;
}

В main, при vec1.resize(3), элементы будут скопированы, потому что Resource не гарантирует, что перемещение не бросит исключения. При vec2.resize(3), элементы будут перемещены, потому что ResourceNoexcept помечен как noexcept, что позволяет вектору безопасно использовать семантику перемещения при переаллокации.