Перегрузка конструктора

В C++ у класса может быть несколько конструкторов, каждый со своими параметрами.

Это так называемая «перегрузка конструктора». Благодаря ей объекты инициализируются по-разному, чем обеспечивается гибкость при их создании.

Начнем с простого примера:

class Rectangle {
private:
double width;
double height;

public:
// Конструктор по умолчанию
Rectangle() : width(1.0), height(1.0) {}

// Конструктор с одним параметром
Rectangle(double size) : width(size), height(size) {}

// Конструктор с двумя параметрами
Rectangle(double w, double h) : width(w), height(h) {}

double area() const {
return width * height;
}
};

int main() {
Rectangle r1; // Используется конструктор по умолчанию
Rectangle r2(5.0); // Используется конструктор с одним параметром
Rectangle r3(3.0, 4.0); // Используется конструктор с двумя параметрами

std::cout << "Area of r1: " << r1.area() << std::endl;
std::cout << "Area of r2: " << r2.area() << std::endl;
std::cout << "Area of r3: " << r3.area() << std::endl;

return 0;
}

Классом Rectangle демонстрируется три различных конструктора, у каждого из которых конкретная цель.

Делегирование конструкторов в версии C++11 и новее

В C++11 появилось делегирование конструкторов: одним конструктором вызывается другой того же класса. Благодаря этому уменьшается дублирование кода:

class Circle {
private:
double radius;
std::string color;

public:
// Основной конструктор
Circle(double r, const std::string& c) : radius(r), color(c) {}

// Делегирующий конструктор
Circle() : Circle(1.0, "white") {}

// Другой делегирующий конструктор
Circle(double r) : Circle(r, "white") {}

double area() const {
return 3.14159 * radius * radius;
}

void display() const {
std::cout << "A " << color << " circle with radius " << radius << std::endl;
}
};

int main() {
Circle c1;
Circle c2(2.5);
Circle c3(3.0, "red");

c1.display();
c2.display();
c3.display();

return 0;
}

Здесь основным конструктором инициализируются radius и color, а другими ему делегируются значения по умолчанию.

Списки инициализаторов

Множественные конструкторы лаконично обрабатываются списками инициализаторов, особенно имея в виду коллекции:

#include <vector>
#include <initializer_list>

class IntegerSet {
private:
std::vector<int> elements;

public:
IntegerSet() {}

IntegerSet(std::initializer_list<int> list) : elements(list) {}

void add(int value) {
elements.push_back(value);
}

void display() const {
for (int elem : elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};

int main() {
IntegerSet set1;
IntegerSet set2 = {1, 2, 3, 4, 5};

set1.add(10);
set1.add(20);

std::cout << "Set 1: ";
set1.display();

std::cout << "Set 2: ";
set2.display();

return 0;
}

В этом примере показано, как конструктором списка инициализаторов создаются объекты с изменяемым числом первоначальных элементов.

Явные конструкторы

Ключевым словом explicit предотвращаются неявные преобразования и инициализация копии.

explicit применяется в конструкторах с одним параметром  —  во избежание нежелательных преобразований типов:

class Box {
private:
double volume;

public:
explicit Box(double v) : volume(v) {}

double getVolume() const { return volume; }
};

void processBox(const Box& box) {
std::cout << "Processing box with volume: " << box.getVolume() << std::endl;
}

int main() {
Box b1(100.0); // OK
processBox(b1); // OK

// Box b2 = 200.0; // Ошибка: инициализация копии недопустима
// processBox(300.0); // Ошибка: неявное преобразование недопустимо

processBox(Box(400.0)); // OK: явная конструкция

return 0;
}

Благодаря explicit предотвращаются малозаметные баги, которые появляются из-за неожиданных неявных преобразований.

Конструкторы копирования и перемещения

Это специальные конструкторы, которыми из имеющихся объектов создаются новые:

#include <cstring>

class String {
private:
char* data;

public:
// Конструктор по умолчанию
String() : data(nullptr) {}

// Конструктор с Си-строкой
String(const char* str) {
if (str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
} else {
data = nullptr;
}
}

// Конструктор копирования
String(const String& other) {
if (other.data) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
} else {
data = nullptr;
}
}

// Конструктор перемещения
String(String&& other) noexcept : data(other.data) {
other.data = nullptr;
}

// Деструктор
~String() {
delete[] data;
}

void display() const {
if (data) {
std::cout << data;
} else {
std::cout << "(empty)";
}
std::cout << std::endl;
}
};

int main() {
String s1("Hello");
String s2 = s1; // Конструктор копирования
String s3 = std::move(s1); // Конструктор перемещения

std::cout << "s1: ";
s1.display();
std::cout << "s2: ";
s2.display();
std::cout << "s3: ";
s3.display();

return 0;
}

В этом примере демонстрируются конструкторы копирования и перемещения, важные для эффективного создания объектов и передачи ресурсов.

Реальный сценарий: класс подключения к базе данных

Рассмотрим практическое применение множественных конструкторов в классе подключения к базе данных:

#include <string>
#include <iostream>

class DBConnection {
private:
std::string host;
int port;
std::string username;
std::string password;
std::string database;

public:
// Конструктор по умолчанию
DBConnection()
: host("localhost"), port(3306), username("root"), password(""), database("") {}

// Конструктор с хостом и портом
DBConnection(const std::string& h, int p)
: DBConnection() {
host = h;
port = p;
}

// Конструктор со всеми параметрами
DBConnection(const std::string& h, int p, const std::string& u,
const std::string& pwd, const std::string& db)
: host(h), port(p), username(u), password(pwd), database(db) {}

// Конструктор со строкой подключения
explicit DBConnection(const std::string& connectionString) {
// Парсинг строки подключения и задание членов
// Это упрощенная версия
size_t pos = connectionString.find(':');
if (pos != std::string::npos) {
host = connectionString.substr(0, pos);
port = std::stoi(connectionString.substr(pos + 1));
}
}

void connect() {
std::cout << "Connecting to " << host << ":" << port
<< " (User: " << username << ", DB: " << database << ")" << std::endl;
// Сам код подключения помещается сюда
}
};

int main() {
DBConnection conn1;
DBConnection conn2("db.example.com", 5432);
DBConnection conn3("db.example.com", 5432, "admin", "secret", "mydb");
DBConnection conn4("192.168.1.100:3307");

conn1.connect();
conn2.connect();
conn3.connect();
conn4.connect();

return 0;
}

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

Производительность

При реализации множественных конструкторов учитываются такие аспекты производительности:

  1. Порядок инициализации: члены инициализируются в порядке их объявления в классе, а не порядке их появления в списке инициализации конструктора.
  2. Аргументы по умолчанию против множественных конструкторов: иногда эти аргументы эффективнее множественных конструкторов.
  3. Делегирование конструктора: несмотря на удобство, злоупотребление этим делегированием сказывается на производительности из-за многочисленных вызовов функций.

Вот пример сравнения аргументов по умолчанию с множественными конструкторами:

class Example {
private:
int a, b, c;

public:
// Использование аргументов по умолчанию
Example(int x = 0, int y = 0, int z = 0) : a(x), b(y), c(z) {}

// Альтернатива: множественные конструкторы
/*
Example() : a(0), b(0), c(0) {}
Example(int x) : a(x), b(0), c(0) {}
Example(int x, int y) : a(x), b(y), c(0) {}
Example(int x, int y, int z) : a(x), b(y), c(z) {}
*/

void display() const {
std::cout << a << ", " << b << ", " << c << std::endl;
}
};

int main() {
Example e1;
Example e2(1);
Example e3(1, 2);
Example e4(1, 2, 3);

e1.display();
e2.display();
e3.display();
e4.display();

return 0;
}

Версия с аргументами по умолчанию лаконичнее и потенциально эффективнее: с нею избегаются повторные определения функций.

Типичные ошибки и как их избежать

  1. Непреднамеренные преобразования. Чтобы избежать неявных преобразований, в конструкторах с одним параметром используйте explicit.
  2. Злоупотребление делегированием конструктора. Это делегирование удобно, но злоупотребление им чревато трудностями понимания кода.
  3. Инициализируются не все члены. Всегда инициализируйте все члены либо в теле конструктора, либо в списке инициализации.
  4. Циклические зависимости при инициализации. Будьте осторожны с ними при инициализации членов.
  5. Игнорирование правила трех/пяти. Правило трех: если определяете оператор присваивания копированием, конструктор копирования или деструктор, реализуйте все три. Правило пяти: в версии C++11 и новее используйте еще и оператор присваивания перемещением, а также конструктор перемещения.

Заключение

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

Понимание этих концепций  —  от простой перегрузки до сложных вроде списков инициализаторов и делегирования конструкторов  —  важно для проектирования надежных и гибких классов.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: C++ Multiple Constructors: Practical Guide

Предыдущая статьяТипы данных JavaScript: передача по значению и ссылке
Следующая статьяИнженерия будущего: интеграция данных, программного обеспечения и искусственного интеллекта