«Из одного конструктора в другой»: вечный вопрос 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+
БлогиРекомендаций по книгамЧитайте также |