← Назад к вопросам

Что требуется, чтобы реаллокация vector использовала move-семантику?

3.0 Senior🔥 81 комментариев
#Язык C++#STL контейнеры и алгоритмы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что требуется, чтобы реаллокация vector использовала move-семантику?

Это важный вопрос о производительности и C++11 move семантике. Ответ касается того, что компилятор должен быть уверен, что move конструктор безопасен.

Основная проблема

Когда vector переполняется (capacity == size) и нужна реаллокация, он:

  1. Выделяет новый блок памяти
  2. Переносит старые элементы
  3. Освобождает старый блок
До реаллокации:
[elem0] [elem1] [elem2] [elem3] [???] [???] [???]
 size=4, capacity=4

Сейчас нужно добавить elem4 - требуется реаллокация!

После реаллокации:
[elem0] [elem1] [elem2] [elem3] [elem4] [???] [???]
 size=5, capacity=8 (обычно двойной размер)

Ошибка на этом этапе может привести к потере исключения!

Исключительная безопасность

Вот почему это важно:

std::vector<MyClass> vec;
vec.push_back(obj1);
vec.push_back(obj2);

// Вектор переполняется
vec.push_back(obj3);  // Требуется реаллокация

// Что если move конструктор выбросит исключение?
// Старые элементы уже перемещены (и исходные объекты разрушены)
// Новые элементы не полностью скопированы
// vector в поврежденном состоянии!

Требование: noexcept

ГЛАВНОЕ ТРЕБОВАНИЕ: move конструктор должен быть noexcept

class Safe {
public:
    Safe(Safe&& other) noexcept {  // КЛЮЧЕВОЕ СЛОВО!
        // Move конструктор не выбросит исключение
    }
};

class Unsafe {
public:
    Unsafe(Unsafe&& other) {  // БЕЗ noexcept
        // Может выбросить исключение!
    }
};

// Использование в vector:
std::vector<Safe> vec1;
vec1.push_back(Safe());  // При реаллокации используется MOVE

std::vector<Unsafe> vec2;
vec2.push_back(Unsafe());  // При реаллокации используется COPY!

Демонстрация поведения

#include <iostream>
#include <vector>

class WithNoExcept {
public:
    WithNoExcept() { std::cout << "Ctor\n"; }
    
    // Move конструктор с noexcept
    WithNoExcept(WithNoExcept&& other) noexcept {
        std::cout << "Move (safe)\n";
    }
    
    // Copy конструктор
    WithNoExcept(const WithNoExcept&) {
        std::cout << "Copy\n";
    }
};

class WithoutNoExcept {
public:
    WithoutNoExcept() { std::cout << "Ctor\n"; }
    
    // Move конструктор БЕЗ noexcept
    WithoutNoExcept(WithoutNoExcept&& other) {  // ОПАСНО!
        std::cout << "Move (unsafe)\n";
    }
    
    WithoutNoExcept(const WithoutNoExcept&) {
        std::cout << "Copy\n";
    }
};

int main() {
    std::vector<WithNoExcept> vec1;
    std::cout << "Safe version:\n";
    for (int i = 0; i < 5; ++i) {
        vec1.push_back(WithNoExcept());
    }
    // Output:
    // Safe version:
    // Ctor
    // Ctor
    // Move (safe)  <- реаллокация использует MOVE
    // Ctor
    // Ctor
    // Move (safe)  <- еще реаллокация
    // Move (safe)
    // Ctor
    // Move (safe)
    // Move (safe)
    // Move (safe)
    
    std::cout << "\nUnsafe version:\n";
    std::vector<WithoutNoExcept> vec2;
    for (int i = 0; i < 5; ++i) {
        vec2.push_back(WithoutNoExcept());
    }
    // Output:
    // Unsafe version:
    // Ctor
    // Ctor
    // Copy  <- реаллокация использует COPY!
    // Ctor
    // Ctor
    // Copy  <- еще реаллокация, еще COPY
    // Copy
    // ...
}

Правильный способ

class MyClass {
private:
    std::unique_ptr<int[]> data;
    size_t size;
    
public:
    MyClass() : size(0) {}
    
    // Конструктор копирования
    MyClass(const MyClass& other) : size(other.size) {
        if (size > 0) {
            data = std::make_unique<int[]>(size);
            std::copy(other.data.get(), other.data.get() + size, data.get());
        }
    }
    
    // Move конструктор с noexcept - КЛЮЧ!
    MyClass(MyClass&& other) noexcept 
        : data(std::move(other.data)), size(other.size) {
        other.size = 0;
    }
    
    // Move assignment
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
            size = other.size;
            other.size = 0;
        }
        return *this;
    }
    
    // Copy assignment
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            MyClass temp(other);
            *this = std::move(temp);
        }
        return *this;
    }
    
    // Деструктор (по умолчанию OK для unique_ptr)
    ~MyClass() = default;
};

Когда vector использует move

Vector использует move вместо copy ТОЛЬКО если:

  1. Move конструктор существует - есть T::T(T&&)
  2. Move конструктор помечен noexcept - T::T(T&&) noexcept
  3. Если оба условия не выполнены - использует copy конструктор
// Проверка: использует ли vector move?
std::vector<MyClass> vec;

// Внутри vector::push_back реаллокация выбирает:
if (std::is_nothrow_move_constructible_v<MyClass>) {
    // Используем move - БЫСТРО
    std::move(old_data, old_data + size, new_data);
} else {
    // Используем copy - МЕДЛЕННО
    std::copy(old_data, old_data + size, new_data);
}

Проверка в compile-time

#include <type_traits>

template<typename T>
void checkMoveSemantics() {
    std::cout << "T move constructible: " 
        << std::is_move_constructible_v<T> << "\n";
    
    std::cout << "T nothrow move constructible: " 
        << std::is_nothrow_move_constructible_v<T> << "\n";
    
    std::cout << "T move assignable: " 
        << std::is_move_assignable_v<T> << "\n";
    
    std::cout << "T nothrow move assignable: " 
        << std::is_nothrow_move_assignable_v<T> << "\n";
}

checkMoveSemantics<std::vector<int>>(); // true, true, true, true
checkMoveSemantics<std::string>();      // true, true, true, true
checkMoveSemantics<MyClass>();          // true, true, true, true (если правильно)

Практические примеры

Пример 1: Wrong - без noexcept

// НЕПРАВИЛЬНО!
class DataContainer {
private:
    std::vector<int> data;
    
public:
    DataContainer(DataContainer&& other) {  // БЕЗ noexcept!
        // Может выбросить исключение от vector
        data = std::move(other.data);
    }
};

std::vector<DataContainer> containers;
containers.push_back(DataContainer());  // Реаллокация использует COPY!

Пример 2: Right - с noexcept

// ПРАВИЛЬНО!
class DataContainer {
private:
    std::vector<int> data;
    
public:
    DataContainer(DataContainer&& other) noexcept {  // С noexcept!
        data = std::move(other.data);
    }
};

std::vector<DataContainer> containers;
containers.push_back(DataContainer());  // Реаллокация использует MOVE!

Пример 3: Стандартные типы

std::vector<std::string> strings;
for (int i = 0; i < 1000; ++i) {
    strings.push_back(std::string("text"));
    // std::string move конструктор noexcept
    // Поэтому используется move!
}

std::vector<std::vector<int>> nested;
for (int i = 0; i < 1000; ++i) {
    nested.push_back(std::vector<int>(100));
    // std::vector move конструктор noexcept
    // Поэтому используется move!
}

Performance Impact

class SlowType {
public:
    SlowType(SlowType&& other) {  // БЕЗ noexcept
        // Copy - медленно!
    }
};

class FastType {
public:
    FastType(FastType&& other) noexcept {  // С noexcept
        // Move - быстро!
    }
};

std::vector<SlowType> slow;  // При реаллокации: Copy, Copy, Copy, Copy
std::vector<FastType> fast;  // При реаллокации: Move, Move, Move, Move

// FastType будет значительно быстрее!

Правило для разработчиков

ПРАВИЛО ОДНОЙ СТРОКИ:

Если вы пишете move конструктор или move assignment оператор - ВСЕГДА добавляйте noexcept

Нет причин для move семантики выбрасывать исключение. Move это просто переход владения ресурсами, который не должен фейлиться.

class Correct {
public:
    Correct(Correct&& other) noexcept = default;
    Correct& operator=(Correct&& other) noexcept = default;
};

Итого

Для того чтобы vector использовал move семантику при реаллокации:

  1. Move конструктор ДОЛЖЕН существовать - либо дефолтный, либо пользовательский
  2. Move конструктор ДОЛЖЕН быть noexcept - это критически важно
  3. Компилятор проверяет std::is_nothrow_move_constructible - и выбирает move или copy
  4. Если вы не добавили noexcept - vector будет консервативен и использует copy

Это гарантирует exception safety - если move выбросит исключение, vector знает, что это ошибка, и не рисует. Поэтому он используют копирование, которое безопаснее.