За замовчуванням мова C++ обробляє будь-який конструктор, як оператор неявної конвертації. Розглянемо наступну програму:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include <iostream> #include <cassert> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Drob(int numerator = 0, int denominator = 1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } // Конструктор копіювання Drob(const Drob ©) : m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) { // Немає необхідності виконувати перевірку denominator тут, тому що ця перевірка вже виконана в конструкторі за замовчуванням std::cout << "Copy constructor worked here!\n"; // просто, щоб показати, що це працює } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); int getNumerator() { return m_numerator; } void setNumerator(int numerator) { m_numerator = numerator; } }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } Drob makeNegative(Drob d) { d.setNumerator(-d.getNumerator()); return d; } int main() { std::cout << makeNegative(7); // передаємо цілочисельне значення return 0; } |
Хоча функція makeNegative() очікує об’єкт класу Drob, ми передаємо їй цілочисельний літерал 7
. Оскільки у класу Drob є конструктор, який може приймати одне цілочисельне значення (конструктор за замовчуванням), то компілятор виконає неявну конвертацію літералу 7
в об’єкт класу Drob. Це робиться шляхом виконання копіюючої ініціалізації параметру d
функції makeNegative() за допомогою конструктора Drob(int, int)
.
Результат виконання програми:
Copy constructor worked here!
-7/1
Неявне перетворення працює для всіх видів ініціалізації (прямої, uniform- і копіюючої).
Конструктори, які використовуються в неявних конвертаціях, називаються конструкторами конвертації (або “конструкторами перетворення”). До C++11 конструкторами конвертації могли бути конструктори тільки з одним параметром. Однак в C++11 це обмеження було знято (разом з доданням uniform-ініціалізації), і конструктори, які мають кілька параметрів, вже також можуть бути конструкторами конвертації.
Ключове слово explicit
Іноді виконання неявних конвертацій може бути доречним, а іноді може бути вкрай небажаним і генерувати несподівані результати:
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 28 29 30 31 32 33 34 |
#include <iostream> #include <string> class SomeString { private: std::string m_string; public: SomeString(int a) // виділяємо рядок розміром a { m_string.resize(a); } SomeString(const char *string) // виділяємо рядок для зберігання значення типу string { m_string = string; } friend std::ostream& operator<<(std::ostream& out, const SomeString &s); }; std::ostream& operator<<(std::ostream& out, const SomeString &s) { out << s.m_string; return out; } int main() { SomeString mystring = 'a'; // виконується копіююча ініціалізація std::cout << mystring; return 0; } |
У прикладі, наведеному вище, ми намагаємося ініціалізувати рядок одним символом типу char. Оскільки змінні типу char є частиною сімейства цілочисельних типів, то компілятор використовуватиме конструктор конвертації SomeString(int)
для неявного перетворення символу типу char в тип SomeString. В результаті змінна типу char конвертується в тип int. А це не зовсім те, що очікується.
Один із способів вирішення цієї проблеми — зробити конструктор явним, використовуючи ключове слово explicit (яке пишеться перед ім’ям конструктора). Явні конструктори (з ключовим словом explicit) не використовуються для неявних конвертацій:
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 28 29 30 31 32 33 34 35 |
#include <iostream> #include <string> class SomeString { private: std::string m_string; public: // Ключове слово explicit робить цей конструктор закритим для виконання будь-яких неявних конвертацій explicit SomeString(int a) // виділяємо рядок розміром a { m_string.resize(a); } SomeString(const char *string) // виділяємо рядок для зберігання значення типу string { m_string = string; } friend std::ostream& operator<<(std::ostream& out, const SomeString &s); }; std::ostream& operator<<(std::ostream& out, const SomeString &s) { out << s.m_string; return out; } int main() { SomeString mystring = 'a'; // помилка компіляції, оскільки SomeString(int) тепер є explicit і, відповідно, недоступний, а іншого підходящого конструктора для конвертації компілятор не бачить std::cout << mystring; return 0; } |
Вищенаведена програма не скомпілюється, так як SomeString(int)
ми зробили явним, а іншого конструктора конвертації, який виконав би неявну конвертацію 'a'
в SomeString, компілятор просто не знайшов.
Однак використання явного конструктора тільки запобігає виконанню неявних конвертацій. Явні конвертації (через оператори явного перетворення) як і раніше дозволені:
1 |
std::cout << static_cast<SomeString>(7); // дозволено: явна конвертація 7 в SomeString через оператор static_cast |
При прямій або uniform-ініціалізації неявна конвертація також виконуватиметься:
1 |
SomeString str('a'); // дозволено |
Правило: Для запобігання виникненню помилок з неявними конвертаціями робіть ваші конструктори явними, використовуючи ключове слово explicit.
Ключове слово delete
Ще одним способом заборонити конвертацію 'a'
в SomeString (неявним або явним способом) є додання закритого конструктора SomeString(char)
:
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 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream> #include <string> class SomeString { private: std::string m_string; SomeString(char) // об'єкти типу SomeString(char) не можуть бути створені поза класом { } public: // Ключове слово explicit робить цей конструктор закритим для виконання будь-яких неявних конвертацій explicit SomeString(int a) // виділяємо рядок розміром a { m_string.resize(a); } SomeString(const char *string) // виділяємо рядок для зберігання значення типу string { m_string = string; } friend std::ostream& operator<<(std::ostream& out, const SomeString &s); }; std::ostream& operator<<(std::ostream& out, const SomeString &s) { out << s.m_string; return out; } int main() { SomeString mystring('a'); // помилка компіляції, оскільки SomeString(char) є private std::cout << mystring; return 0; } |
Тим не менш, цей конструктор все ще може використовуватися всередині класу (private закриває доступ до даних тільки для об’єктів поза тілом класу).
Краще рішення — використовувати ключове слово delete (додане в C++11) для видалення цього конструктора:
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 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> #include <string> class SomeString { private: std::string m_string; public: SomeString(char) = delete; // будь-яке використання цього конструктора призведе до помилки // Ключове слово explicit робить цей конструктор закритим для виконання будь-яких неявних конвертацій explicit SomeString(int a) // виділяємо рядок розміром a { m_string.resize(a); } SomeString(const char *string) // виділяємо рядок для зберігання значення типу string { m_string = string; } friend std::ostream& operator<<(std::ostream& out, const SomeString &s); }; std::ostream& operator<<(std::ostream& out, const SomeString &s) { out << s.m_string; return out; } int main() { SomeString mystring('a'); // помилка компіляції, оскільки SomeString(char) видалений std::cout << mystring; return 0; } |
Після видалення функції, будь-яке її використання викличе помилку компіляції.
Зверніть увагу, конструктор копіювання і перевантажені оператори також можуть бути видалені за допомогою delete для запобігання їх використанню.