Как стать автором
Обновить

Создание объектов без конструктора по умолчанию в C++: искусство владения памятью

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров741

В C++ мы привыкли к тому, что создание объекта неизбежно связано с вызовом конструктора. Однако иногда возникают ситуации, когда конструктор по умолчанию отсутствует или удален (= delete), а нам всё равно нужно получить корректный объект. Это может быть особенно актуально при работе с системным программированием, сериализацией/десериализацией, оптимизациями под производительность или даже при реализации своих собственных контейнеров.

В данной статье рассматриваются методы создания объектов без использования конструктора по умолчанию с использованием возможностей стандарта C++17 , который предоставляет гибкие инструменты управления памятью и типобезопасностью. Мы рассмотрим техники, которые позволяют работать с такими объектами напрямую, сохраняя контроль над процессом инициализации и временем жизни объектов.

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

Проблема: удалённый конструктор по умолчанию

Рассмотрим простую структуру:

struct A {
    A() = delete;
    int x;
};

Попробуем создать объект:

int main() {
  A a; // Ошибка: использование удалённого конструктора
}

Компилятор блокирует создание объекта, потому что конструктор был явно удалён. Однако на самом деле память под A всё ещё можно выделить — просто компилятор не даст нам вызвать конструктор. А что если мы хотим создать объект без вызова конструктора?

Погружение: когда конструктор мешает

Иногда наличие обязательного конструктора становится препятствием, особенно в следующих задачах:

  • Сериализация/десериализация (например, восстановление состояния из бинарного файла).

  • Работа с отображённой в память областью (memory-mapped I/O).

  • Реализация собственных контейнеров или аллокаторов.

  • Оптимизация производительности (отложенная инициализация).

Решение 1: Ручное управление памятью через placement new

C++ предоставляет механизм placement new, который позволяет размещать объекты в уже выделенной памяти без дополнительного выделения:

alignas(A) char buffer[sizeof(A)];
A* a = new(buffer) A; // placement new

Однако это тоже вызывает конструктор. Если он удален, то этот код не скомпилируется.

Решение 2: Обход конструктора через union

Можно воспользоваться union’ами, где один из членов используется только для заполнения памяти:

union storage_t {
    A a;
    char buffer[sizeof(A)];
};

storage_t s;
new(&s.a) A(); // снова placement new

Но это работает только если конструктор существует. Если он удален — такой способ тоже не пройдёт.

Решение 3: Использование std::launder и raw-памяти (без вызова конструктора)

Интереснее становится ситуация, когда мы используем char[] для хранения памяти и интерпретируем её как нужный тип с помощью std::launder.

std::launder необходим, начиная с C++17, чтобы помочь компилятору правильно обрабатывать aliasing и оптимизации.

Пример:

alignas(A) char data[sizeof(A)];
A* a = std::launder(reinterpret_cast<A*>(data));

Важно: мы не создаём объект в строгом смысле. Это UB по стандарту, если объект не был создан явно через placement new или иной способом, который инициализирует объект. Но если наш тип POD (Plain Old Data), то такое поведение часто работает в реальных реализациях.

Реализация pod_storage: безопасное хранение POD-типов без конструкторов

Мы можем создать шаблонную обёртку, которая обеспечивает доступ к raw-памяти, интерпретируя её как объект нужного типа:

template <typename T>
class pod_storage {
    alignas(T) char data_[sizeof(T)];

public:
    T* get() noexcept {
        return std::launder(reinterpret_cast<T*>(data_));
    }

    const T* get() const noexcept {
        return std::launder(reinterpret_cast<const T*>(data_));
    }

    T* operator->() noexcept { return get(); }
    const T* operator->() const noexcept { return get(); }

    T& operator*() noexcept { return *get(); }
    const T& operator*() const noexcept { return *get(); }
};

Это позволяет нам работать с типами, у которых нет конструктора.

Расширение идеи: Перегрузка операторов, обнуление памяти, поддержка статического и динамического массива с пользовательским аллокатором

#include <iostream>
#include <memory>
#include <type_traits>
#include <cstddef> 
#include <cstring>
#include <vector>

// ==============================
// 1. pod_storage — хранение одного объекта без конструктора
template <typename T>
class pod_storage {
    alignas(T) std::byte data_[sizeof(T)];

public:
    pod_storage() noexcept {
        std::memset(data_, 0, sizeof(data_));
    }

    T* get() noexcept {
        return std::launder(reinterpret_cast<T*>(data_));
    }

    const T* get() const noexcept {
        return std::launder(reinterpret_cast<const T*>(data_));
    }

    T* operator->() noexcept { return get(); }
    const T* operator->() const noexcept { return get(); }

    T& operator*() noexcept { return *get(); }
    const T& operator*() const noexcept { return *get(); }
};

// ==============================
// 2. pod_array — статический массив без конструкторов
template <typename T, std::size_t N>
class pod_array {
    alignas(T) std::byte data_[N * sizeof(T)];

public:
    pod_array() noexcept {
        std::memset(data_, 0, sizeof(data_));
    }

    T* data() noexcept {
        return std::launder(reinterpret_cast<T*>(data_));
    }

    const T* data() const noexcept {
        return std::launder(reinterpret_cast<const T*>(data_));
    }

    T& operator[](std::size_t i) noexcept {
        return data()[i];
    }

    const T& operator[](std::size_t i) const noexcept {
        return data()[i];
    }

    static constexpr std::size_t size() noexcept {
        return N;
    }
};

// ==============================
// 3. Пользовательский аллокатор
template <typename T>
class pod_allocator {
public:
    using value_type = T;

    pod_allocator() = default;
    template <typename U>
    pod_allocator(const pod_allocator<U>&) noexcept {}

    [[nodiscard]] T* allocate(std::size_t n) {
        void* ptr = std::malloc(n * sizeof(T));
        if (!ptr) throw std::bad_alloc();
        return static_cast<T*>(ptr);
    }

    void deallocate(T* ptr, std::size_t /*n*/) noexcept {
        std::free(ptr);
    }
};

template <typename T, typename U>
constexpr bool operator==(const pod_allocator<T>&, const pod_allocator<U>&) noexcept {
    return true;
}

template <typename T, typename U>
constexpr bool operator!=(const pod_allocator<T>& lhs, const pod_allocator<U>& rhs) noexcept {
    return !(lhs == rhs);
}

// ==============================
// 4. dynamic_pod_array — динамический массив с аллокатором
template <typename T, typename Allocator = std::allocator<T>>
class dynamic_pod_array {
private:
    T* data_;
    std::size_t size_;
    Allocator alloc_; // Используем пользовательский аллокатор

public:
    explicit dynamic_pod_array(std::size_t n)
        : size_(n), data_(alloc_.allocate(n)) {
        std::memset(data_, 0, n * sizeof(T));
    }

    ~dynamic_pod_array() {
        // Не вызываем деструкторы (POD)
        alloc_.deallocate(data_, size_);
    }

    T* data() noexcept { return data_; }
    const T* data() const noexcept { return data_; }

    T& operator[](std::size_t i) noexcept {
        return data()[i];
    }

    const T& operator[](std::size_t i) const noexcept {
        return data()[i];
    }

    [[nodiscard]] std::size_t size() const noexcept {
        return size_;
    }

    [[nodiscard]] const Allocator& get_allocator() const noexcept {
        return alloc_;
    }
};
struct Point {
    int x, y;
    Point() = delete; // Конструктор удален
};

int main() {

    {
        // Используем pod_storage
        pod_storage<Point> PointStorage;

        // Работаем через operator->
        PointStorage->x = 42;
        std::cout << "PointStorage->x = " << PointStorage->x << '\n';

        // Работаем через operator*
        std::cout << "(*PointStorage).x = " << (*PointStorage).x << '\n';

        // Можно получить указатель напрямую
        Point* p = PointStorage.get();
        p->x = 100;
        std::cout << "p->x = " << p->x << '\n';
    }

    // Используем pod_array
    {
        pod_array<Point, 3> points;

        points[0] = { 1, 2 };
        points[1] = { 3, 4 };
        points[2] = { 5, 6 };

        for (size_t i = 0; i < points.size(); ++i) {
            std::cout << "pod_array[" << i << "] = {"
                << points[i].x << ", " << points[i].y << "}\n";
        }
    }

    // Используем std::allocator через dynamic_pod_array
    {
        dynamic_pod_array<int> arr(4);
        for (size_t i = 0; i < arr.size(); ++i) {
            arr[i] = static_cast<int>(i * 10);
        }

        std::cout << "Using std::allocator:\n";
        for (size_t i = 0; i < arr.size(); ++i) {
            std::cout << "arr[" << i << "] = " << arr[i] << '\n';
        }
    }

    // Используем pod_allocator
    {
        dynamic_pod_array<Point, pod_allocator<Point>> arr(3);

        arr[0] = {1, 2};
        arr[1] = {3, 4};
        arr[2] = {5, 6};

        std::cout << "Using pod_allocator:\n";
        for (size_t i = 0; i < arr.size(); ++i) {
            std::cout << "Point[" << i << "] = {" 
                      << arr[i].x << ", " << arr[i].y << "}\n";
        }
    }

    return 0;
}
Ссылка на полный код

Полный код тут: QbitQuantum/trivial_storage

Заключение

Программирование — это не только про то, как решить задачу, но и про то, чтобы попробовать сделать то, что «нельзя». Порой самые интересные решения рождаются именно там, где стандартные инструменты заканчиваются.

И если нельзя — не значит невозможно. Просто нужно найти свой путь.

Теги:
Хабы:
+2
Комментарии14

Публикации

Работа

Программист C++
103 вакансии
QT разработчик
8 вакансий

Ближайшие события