По умолчанию 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 <cassert> #include <iostream> 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 <string> #include <iostream> 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. Результат — не то, что нужно (неожиданный).
Один из способов решения этой проблемы — сделать конструктор явным, используя ключевое слово 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 <string> #include <iostream> 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 компилятор просто не нашел.
Однако использование явного конструктора только предотвращает выполнение неявных преобразований. Явные конвертации (через операторы cast) по-прежнему разрешены:
1 |
std::cout << static_cast<SomeString>(7); // Разрешено: Явное преобразование 7 в SomeString через оператор 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 <string> #include <iostream> 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 <string> #include <iostream> 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 для предотвращения их использования.
Привет! Мне нравится визуальный стиль оформления твоего листинга. Не мог бы ты скинуть данные по этому стилю, хочу его в CodeBlock засунуть.
Привет. Тема Monokai. Вот ссылка на цвета — Monokai.