Конструктор вызывает конструктор: цепочки в C++ — можно ли?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Из одного конструктора в другой»: вечный вопрос C++ разработчика

Представьте ситуацию: вы пишете класс с несколькими конструкторами. Один принимает три параметра, другой — два, третий — один. И во всех повторяется одна и та же логика инициализации. Руки чешутся написать что-то вроде MyClass() : MyClass(42, "default") {} — и забыть о дублировании. Но компилятор C++ (до определённого момента) смотрит на это косо. Можно ли вообще вызывать конструктор из другого конструктора? И если да — то как?

Этот вопрос — один из самых популярных на Stack Overflow (десятки тысяч просмотров). Неудивительно: тема цепочек конструкторов (constructor chaining) — настоящий камень преткновения для новичков и даже опытных разработчиков, переходящих на C++ с Java или Python. В этой статье мы разберём три подхода к решению проблемы, от «костылей» до элегантных решений C++11, и выясним, какой из них стоит использовать в реальном коде.

Почему нельзя просто вызвать конструктор из конструктора?

Наивный подход выглядит так:

class MyClass {
public:
    MyClass() {
        // какой-то код
    }
    MyClass(int x) {
        MyClass(); // ОШИБКА: так нельзя!
    }
};

В C++ конструктор — не обычная функция. Он инициализирует объект один раз. Если вы попытаетесь явно вызвать другой конструктор внутри тела, компилятор воспримет это как создание временного объекта — и ничего не инициализируется в текущем экземпляре. Результат: либо ошибка компиляции, либо (что хуже) неопределённое поведение.

До стандарта C++11 единственным легальным способом «разделить» логику инициализации был вызов приватного метода init(). Но это не было настоящим делегированием: члены класса сначала инициализировались конструктором по умолчанию, а потом перезаписывались. Неэффективно и чревато багами.

Подход №1: делегирующие конструкторы (C++11 и новее)

Начиная с C++11, в языке появилась долгожданная фича — делегирующие конструкторы (delegating constructors). Теперь можно вызвать один конструктор из другого прямо в списке инициализации:

class MyClass {
    int value;
    std::string name;
public:
    // Базовый конструктор
    MyClass(int v, const std::string& n) : value(v), name(n) {
        std::cout << "Full init: " << value << ", " << name << "\n";
    }
    // Делегирующий конструктор
    MyClass(int v) : MyClass(v, "default") {}
    // Ещё один делегирующий
    MyClass() : MyClass(0, "default") {}
};

Как это работает? Конструктор MyClass(int v) передаёт управление конструктору MyClass(int, string). Тот выполняет всю инициализацию, и только после этого управление возвращается обратно. Важное правило: в списке инициализации нельзя смешивать вызов другого конструктора с прямой инициализацией членов. Либо делегирование, либо своя инициализация — одно из двух.

Плюсы и минусы

  • ✅ Элегантность: код становится чистым, без дублирования.
  • ✅ Производительность: объект инициализируется один раз, без лишних операций.
  • ⚠️ Ограничение: делегирование работает только в одну «глубину» — цепочка не может быть рекурсивной (вы вызовете переполнение стека).
  • ⚠️ Совместимость: требует C++11 или новее. Если вы застряли на древнем компиляторе — этот вариант не для вас.

Подход №2: приватный метод init()

Классический «до-C++11» способ — вынести общую логику в приватный метод. Выглядит так:

class MyClass {
    int value;
    std::string name;
    void init(int v, const std::string& n) {
        value = v;
        name = n;
        std::cout << "Init called\n";
    }
public:
    MyClass(int v, const std::string& n) {
        init(v, n);
    }
    MyClass(int v) {
        init(v, "default");
    }
    MyClass() {
        init(0, "default");
    }
};

На первый взгляд — работает. Но есть существенный недостаток: члены класса сначала инициализируются значениями по умолчанию (для int — 0, для string — пустая строка), а потом перезаписываются. Если инициализация сложная (например, выделение памяти), это двойная работа. К тому же, если какой-то член — константа или ссылка, такой трюк вообще не пройдёт:

class MyClass {
    const int id; // Ошибка! const нужно инициализировать в списке инициализации
    // ...
};

Когда это оправдано?

  • Вы пишете код для C++98/03.
  • Инициализация членов тривиальна (int, float, простые типы).
  • Нет const/ref членов и нет виртуальных базовых классов.

В остальных случаях лучше избегать этого подхода — он чреват багами и усложняет поддержку кода.

Подход №3: виртуальные конструкторы с фабричным методом

Иногда требуется не просто «вызвать конструктор из конструктора», а создать объект определённого типа в зависимости от аргументов. Это уже не совсем цепочка, а паттерн Factory Method. Но раз уж мы говорим о гибкости инициализации — стоит упомянуть.

class Base {
public:
    virtual void init() = 0;
    virtual ~Base() = default;
};

class DerivedA : public Base {
public:
    DerivedA() { std::cout << "DerivedA created\n"; }
    void init() override { /* логика A */ }
};

class DerivedB : public Base {
public:
    DerivedB() { std::cout << "DerivedB created\n"; }
    void init() override { /* логика B */ }
};

Base* createObject(int type) {
    switch(type) {
        case 1: return new DerivedA();
        case 2: return new DerivedB();
        default: return nullptr;
    }
}

Этот подход решает другую задачу — выбор конструктора на этапе выполнения. Он не заменяет делегирование, но может комбинироваться с ним. Например, вы можете сделать фабрику, которая внутри использует делегирующие конструкторы.

Сравнение подходов: что выбрать?

Давайте сведём всё в таблицу для наглядности:

Критерий Делегирующие конструкторы (C++11) Метод init() Фабричный метод
Стандарт C++ C++11+

Блоги

Рекомендаций по книгам