По умолчанию язык 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 для предотвращения их использования.
Почему не используется этот конструктор
Ведь мы передаем переменную типа char ?
Потому что в втором конструкторе аргумент(const char*) это указатель на char а не (char), нужно передать ссылку, и изменить сам конструктор
с этого:
на
тогда все заработает. )
Но мы же не можем получить адрес ‘a’ — это r-value
Как передать в этот конструктор
Только создать переменную типа char, и передать уже её адрес?
Я тоже думал, что explicit это стандартный с++. да. и не все конструкторы следует писать с этим оператором. например не следует писать конструктор копирования с explicit. У компилятора сразу мозги на бикрень но ошибки не выдаст. Но когда запустите начнётся настоящая галиматья, которая проходит если убрать этот оператор.
Я правильно поняла, что explicit спасёт от неявных конвертаций только при копирующей инициализации? В топку тогда такое ключевое слово, которое только при одном из трёх способов помогает.
Насчёт "= delete" тоже как-то резануло… Почему нельзя было поставить это слово перед конструктором, как обычно
Что-то пошло не так.
Фраза "Это делается путем выполнения копирующей инициализацией параметра d функции makeNegative() с помощью конструктора Drob(int, int)." не соответствует действительности.
Добавляем деструктор с выводом и отладочную печать в контрольных точках.
И получаем результат:
Constructor worked here!
Enter
Leave
Copy constructor worked here!
Destructor
-7/1
Destructor
Exit
При вызове makeNegative(7) работает обычный конструктор, создавая анонимный объект. Объект передается в функцию. В функции происходит обработка. При выходе выполняется копирующая инициализация второго анонимного объекта. Первый объект удаляется. Производится вывод. Удаляется второй анонимный объект.
да, Ваша демонстрация показательней, чем в уроке.
При вызове makeNegative(7) компилируется в вызов makeNegative(), без аргументов, а 7 перейдёт в параметр, получится Drob makeNegative(Drob d = 7), а Drob d = 7 — это "копирующая инициализация" как и написано в тексте урока, только 7 это не объект, поэтому вместо к.копирования будет вызван к.преобразования(он же обычный к.по умолчанию), и только потом к.копирования при возврате объекта. В предыдущем уроке два раза вызывался к.копирования.
Ваш пример с выводом действительно лучше, чем в уроке, так как видно что вместо к.копирования сработал к.преобразования о котором и идёт речь.
Всё верно.
Конструктор копирования вызывается, только если уже есть объект, который нужно скопировать.
А 7, это r-value, просто копирующая инициализация.
А я думал, что конструктор копирования — это когда аргументом является такой же тип, как и у хозяина конструктора.
MyClass () — конструктор по умолчанию (для инициализации массивов структур и классов)
MyClass (YourClass, TheirClass) — конструктор инициализации (два+ аргумента)
MyClass (YourClass) — Конструктор преобразования (вот не знаю обязательно ли ссылка)
MyClass (MyClass&) — Конструктор копирования (обязательно ссылка)
Привет! Мне нравится визуальный стиль оформления твоего листинга. Не мог бы ты скинуть данные по этому стилю, хочу его в CodeBlock засунуть.
Привет. Тема Monokai. Вот ссылка на цвета — Monokai.
А шрифт какой?