На цьому уроці ми розглянемо, що таке делегуючі конструктори в мові С++, навіщо вони були придумані і як їх використовувати.
Проблема
При створенні нового об’єкта класу, компілятор мови C++ неявно викликає конструктор цього об’єкту. Не рідкість зустріти клас з декількома конструкторами, які частково виконують одне і те ж, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Boo { public: Boo() { // Частина коду X } Boo(int value) { // Частина коду X // Частина коду Y } }; |
Тут є 2 конструктори: конструктор за замовчуванням і конструктор, який приймає цілочисельне значення. Оскільки Частина коду X
потрібна обом конструкторам, то вона дублюється в кожному з них.
А як ви вже могли здогадатися, дублювання коду — це те, чого слід уникати, тому давайте розглянемо можливі рішення цієї проблеми.
Рішення в C++11
Непогано було б, щоб конструктор Boo(int)
викликав конструктор Boo() для виконання Частина коду X
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Boo { public: Boo() { // Частина коду X } Boo(int value) { Boo(); // використовуємо вищенаведений конструктор для виконання частини коду X // Частина коду Y } }; |
Або:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Boo { public: Boo() { // Частина коду X } Boo(int value): Boo() // використовуємо вищенаведений конструктор для виконання частини коду X { // Частина коду Y } }; |
Однак, якщо ваш компілятор не сумісний з C++11, і ви спробуєте викликати один конструктор всередині іншого конструктора, то це скомпілюється, але працюватиме не так, як ви очікуєте.
До C++11 явний виклик одного конструктора з іншого призводить до створення тимчасового об’єкта, який потім ініціалізується за допомогою конструктора цього об’єкту і ігнорується, залишаючи вихідний об’єкт незмінним.
Використання окремого методу
Конструкторам дозволено викликати інші методи класу, які не є конструкторами. Хоча у вас може виникнути спокуса скопіювати код з першого конструктора в другий конструктор, наявність дубльованого коду зробить ваш клас важчим для розуміння і обтяжливішим для підтримки. Кращим рішенням буде створення окремого методу (не конструктора), який виконуватиме загальну ініціалізацію, і обидва конструктори викликатимуть цей метод. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Boo { private: void DoX() { // Частина коду X } public: Boo() { DoX(); } Boo(int nValue) { DoX(); // Частина коду Y } }; |
Тут ми звели дублювання коду до мінімуму.
Крім того, ви можете опинитися в ситуації, коли вам потрібно буде написати метод для повторної ініціалізації класу назад до значень за замовчуванням. Оскільки у вас, ймовірно, вже є конструктор, який це робить, то у вас може виникнути спокуса спробувати викликати цей конструктор з вашого методу. Однак це призведе до несподіваних результатів. Багато розробників просто копіюють код з конструктора в функцію ініціалізації — це спрацює, але призведе також до дублювання коду. Кращим рішенням буде перемістити код з конструктора в вашу нову функцію і змусити конструктор викликати вашу нову функцію для виконання ініціалізації:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Boo { public: Boo() { Init(); } Boo(int value) { Init(); // Робимо що-небудь з value } void Init() { // Код ініціалізації Boo } }; |
Тут ми підключаємо функцію Init() для ініціалізації змінних-членів назад значеннями за замовчуванням, а потім кожен конструктор викликає функцію Init() перед своїм фактичним виконанням. Це зменшує дублювання коду до мінімуму і дозволяє явно викликати Init() з будь-якого місця в програмі.
Делегуючі конструктори
Починаючи з C++11, конструкторам дозволено викликати інші конструктори. Цей процес називається делегуванням конструкторів (або “ланцюжком конструкторів”). Щоб один конструктор викликав інший, потрібно просто зробити виклик цього конструктора в списку ініціалізації членів. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Boo { private: public: Boo() { // Частина коду X } Boo(int value): Boo() // використовуємо конструктор за замовчуванням Boo() для виконання частини коду X { // Частина коду Y } }; |
Все працює як потрібно. Переконайтеся, що ви викликаєте конструктор зі списку ініціалізації членів, а не з тіла конструктора.
Ось ще один приклад використання делегуючих конструкторів для зменшення дубльованого коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include <string> class Employee { private: int m_id; std::string m_name; public: Employee(int id=0, const std::string &name=""): m_id(id), m_name(name) { std::cout << "Employee " << m_name << " created.\n"; } // Використовуємо делегуючі конструктори для зменшення дубльованого коду Employee(const std::string &name) : Employee(0, name) { } }; int main() { Employee a; Employee b("Ivan"); return 0; } |
Цей клас має 2 конструктори (один з яких викликає іншого). Таким чином, кількість дубльованого коду зменшено (нам потрібно записати тільки одне визначення конструктора замість двох).
Декілька заміток про делегуючі конструктори
По-перше, конструктору, який викликає іншого конструктора, забороняється виконувати будь-яку ініціалізацію членів класу. Тому конструктори можуть або викликати інші конструктори, або виконувати ініціалізацію, але не все відразу.
По-друге, один конструктор може викликати іншого конструктора, в коді якого може знаходитися виклик першого конструктора. Це створить нескінченний цикл і призведе до того, що пам’ять стеку закінчиться і відбудеться збій. Ви можете уникнути цього, переконавшись, що в конструкторі, який викликається, немає виклику першого (і взагалі будь-якого іншого) конструктора. Будьте обережні і не використовуйте вкладені виклики конструкторів.