В С++ есть так называемый конвертирующий конструктор. Это такой конструктор, который преобразует входящие в себя типы в типы своего класса:
C++
1
2
3
4
5
6
7
8
9
10
11
//Листинг #1.1 Конвертирующий конструктор
classMyClass{
intx;
public:
MyClass(intvalue){}//Конструктор с одним параметром
};
intmain(){
MyClass obj=100;//<-- Сначала int (тип сотни) приводится к типу MyClass и только потом присваивается в obj
}
Конструктор, объявленный без спецификатора-функции explicit, задает преобразование типов его параметров к типу своего класса. Такой конструктор называется конструктором преобразования (converting constructor).
До принятия стандарта С++11 преобразование посредством конструктора задавалось с использованием конструктора с одним параметром.
Со времени принятия стандарта С++11 количество параметров конвертирующего конструктора не ограничивается.
Конструктор преобразования ведёт себя как функция, автоматически преобразующая типы, входящие в себя, к своему родному типу.
Конструктор преобразования отличается от обычного конструктора с параметрами тем, что провоцирует неявные приведения типов входящих в себя параметров к типу МойКласс.
Неявные преобразования иногда удобны, а иногда вредны. Новичкам обычно сложно быстро привыкнуть к чудесам из-за неявных преобразований. В С++ даже два полностью идентичных класса компилятором считаются неодинаковыми типами. Из-за этого нельзя делать вот так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #a1
classX{
intvalue;
};
classY{
intvalue;
};
intmain(){
Xx;//Объект класса X
Yy;//Объект класса Y
x=y;//Ошибка компиляции
}
На самом деле можно довести этот код до состояния, что присваивание, провоцирующее ошибку, будет полноценно работать. В данной ситуации не работает, потому что созданные объекты имеют разные типы. Типы эквивалентны, но разные.
Благодаря существованию конструктора преобразования (converting constructor) можно легко обогнуть возникновение подобной проблемы.
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
//Листинг #a2
classY;//Прототипы классов
classX;
classX{
intvalue;
public:
X(){}//конструктор по умолчанию
X(Yz);//конвертирующий конструктор
};
classY{
intvalue;
};
X::X(Yz){//конвертирующий конструктор вынесен за пределы класса
}
intmain(){
Xx;
Yy;
x=y;//теперь это позволено
}
Мы научили класс принимать в себя чужеродный класс так, как будто бы принимаемый класс был принимающему классу родным. Но этого мало, надо научить принимающий класс переносить в себя нужные данные от принимаемой стороны. Немного разовьём классы, чтобы они умели делать что-то полезное, и научим класс X принимать в себя данное из Y:
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
//Листинг #a3
#include <iostream>
usingnamespacestd;
classY;//Прототипы классов
classX;
classX{
intvalue;
public:
X(){}//конструктор по умолчанию
X(Yz);//конвертирующий конструктор
voidprint(){
cout<<value<<'\n';//выводим данные класса на экран
}
};
classY{
intvalue;
public:
Y(){//конструктор по умолчанию
value=10;//записываем в value значение 10
}
intget(){//метод, возвращающий значени value из Y
returnvalue;//возвращаем
}
};
X::X(Yparam){//конвертирующий конструктор вынесен за пределы класса
value=param.get();//принимаем входящее в конструктор значение, запоминаем его в param и подбираем из param переменной value, принадлежащей классу X
}
intmain(){
Xx;
Yy;
x=y;//теперь это позволено
x.print();//выводим, видим 10
cin.get();
}
Тут ничего сложного нет, ключи к пониманию — знание процесса передачи значения транзитом через конструктор в класс, общее понимание методов класса и параметров функций.
На самом деле пример можно было упростить, но я решил использовать именно такой, а не более простой, потому что более простой вариант, как я могу догадываться, ясности даст меньше. Выглядит он так:
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
//Листинг #a4
#include <iostream>
usingnamespacestd;
classMyClass{
intvalue;
public:
MyClass(){}
MyClass(intx){
value=x;
}
voidprint(){
cout<<value;
}
};
intmain(){
MyClass obj;
obj=100;//Сначала int приводится к типу MyClass, потом полученный объект уходит в конструктор MyClass(int)
obj.print();
}
В листинге #a4 происходит то же, что и было показано ранее. Несмотря на несответствие типов присваивание работает так, как будто бы типы были одинаковые. Происходит это из-за неявного приведения, получаемого в результате работы конвертирующего конструктора. Как вы можете видеть, для параметра конвертирующего конструктора имеет значение тип принимаемой стороны. В начале типом был наш собственный класс, а в этом коде типом был int. Этот тип — это тип от присваиваемого класс объекта значения. Программисту нужно только научить принимающий класс принимать данные из чужого класса.
В некоторых ситуациях неявное приведение нежелательно.
Если мы явно не хотим возможности присвоения в объект объектов любого типа, отличного от типа самого объекта, если мы вообще не думаем о том, чтобы присваивать в объект объекты любого другого типа, нужно отключать конвертирующий конструктор, получить просто конструктор с параметрами. Это касается не только операции присвоения, но и вообще любой операции, которая будет доступна объекту класса (сложение, вычитание, умножение…).
Отключается конвертирующий конструктор с помощью ключевого слова explicit.
Отключение конвертирующего конструктора даёт программисту возможность большего контроля над ходом работы программы.
Иногда неявные преобразования приносят пользу, но иногда могут привести к незаметным, но серьёзным ошибкам в коде. Как правило, неявных преобразований типов, порождаемых конструктором конвертирования, как раз избегают.
Немного сложно смоделировать наглядную иллюстрацию возможного возникновения проблемы. Вспомним, как начинали изучать С++, где неявные приведения буквально удивляли нас:
C++
1
2
3
4
intx=10;
inty=3;
doubleres=x/y;//получается 3
Иногда это действительно могло быть полезно, но обычно приводило к неприятностям. То же самое и с неявными приведениями, проводимыми посредством конвертирующего конструктора. Оно вроде бы и удобно, но может приводить к неправильной работе программы, а поймать такую ошибку в случае с самописными классами сложнее, чем в примитивном варианте по примеру int и double. В общем, пока не понимаем как происходят приведения, неявные приведения легко оказываются занозой: любой инструмент нужно уметь использовать.
Отключаем конструктор конвертирования:.
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
//Листинг #a5
#include <iostream>
usingnamespacestd;
classMyClass{
intvalue;
public:
MyClass(){}
explicitMyClass(intx){//приписали к конструктору конвертирования explicit
value=x;
}
voidprint(){
cout<<value;
}
};
intmain(){
MyClass obj(200);//Теперь можно только так, или делать перегрузку операций
obj=100;//конструктор преобразования был отключен, теперь это не работает
obj.print();
}
Благодаря отключению конструктора автоматического приведения типа поведение программы больше предсказуемо, теперь программисту легче контроллировать процесс работы, потому что неявности сомнительного характера больше нет.
Иногда удобно использовать конструктор автоматических преобразований, но сохранять при этом полный контроль над ходом работы программы. Этого можно добиться при помощи вспомогательных функций. Возможно делать так:
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
//Листинг #a6
#include <iostream>
usingnamespacestd;
classMyClass{
intvalue;
MyClass(intx){//конвертирующий конструктор закрыт в приватной секции класса
value=x;
}
public:
MyClass(){}//конструктор по умолчанию
voidprint(){//метод вывода данных класса на экран
cout<<value;
}
staticMyClass convert(intparam){//вспомогательная функция для использования конвертирующего конструктора
returnMyClass(param);
}
};
intmain(){
MyClass obj;//Теперь можно только так, или делать перегрузку операций
obj=MyClass::convert(100);//конструктор преобразования был отключен, теперь это не работает
obj.print();
};
Статическая функция в листинге #a6 нужна, чтобы не создавать объекта класса для вызова той функции. Эта функция выполняет роль посредника между конструктором конвертирования, сокрытым в секции private и уходящим в конструктор конвектирования параметром. Это как с обычными конструкторами: если что-то сокрыто в секции private, то нужен посредник для взаимодействия этого чего-то со внешним для класса миром. Но этот код имеет и недостаток, ведь теперь мы не сможем использовать конструктор при объявлении, используя круглые скобки наиболее привычным образом:
C++
1
2
3
intmain(){
MyClass obj(200);//<-- будет ошибка компиляции
}
Ошибка, потому что конструктор, который мы инициируем, закрыт от прямого доступа в секции private. На самом деле это обходится перегрузкой операции, но перегрузка операций выходит за рамки этой статьи. Просто имейте в виду, что есть вот такой вариант, который является некоторым компромиссом между использованием explicit и отказа от explicit.
Конвертирующий конструктор способен зарождать умеющую маскироваться ошибку: код компилируется и работает, но в самый неподходящий момент, по закону подлости, ошибка скажет своё слово.
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
//Пример маскирующейся ошибки, порождаемой конвертирующим конструктором Основной код
#include <iostream>
usingnamespacestd;
classString//класс в проекте
{
public:
String(size_t count_,charch='\0'){};
//... остальное
};
voiddo_something(Stringconst& x){};//и такие вот функции
voiddo_something(int){};
intmain(){
//функции, которые отмечены выше,
//на каком-то этапе работы программы
//выполняются много раз
StringS(10);
inti=0;
do_something(S);//Код, конечно, так не пишут
do_something(i);//Это, чтобы имитировать
do_something(S);//многократное выполнени
do_something(i);//этих функций
do_something(S);
do_something(i);
}
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
#include <iostream>
usingnamespacestd;
classString//класс в проекте
{
public:
String(size_t count_,charch='\0'){};
//... остальное
};
voiddo_something(Stringconst& x){
cout << "1";
}
voiddo_something(int){
cout<<"2\n";
}
intmain(){
//функции, которые отмечены выше,
//на каком-то этапе работы программы
//выполняются много раз
StringS(10);
inti=0;
do_something(S);
do_something(i);
do_something(S);
do_something(i);
do_something(S);
do_something(i);
}
ИЗМЕНЕНИЕ ==>
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
#include <iostream>
usingnamespacestd;
classString//класс в проекте
{
public:
String(size_t count_,charch='\0'){};
//... остальное
};
voiddo_something(Stringconst& x){
cout << "1";
}
voiddo_something_with_int(int){
cout<<"2\n";
}
intmain(){
//функции, которые отмечены выше,
//на каком-то этапе работы программы
//выполняются много раз
StringS(10);
inti=0;
do_something(S);
do_something(i);
do_something(S);
do_something(i);
do_something(S);
do_something(i);
}
Ошибку даже в маленьком коде сложно заметить. Этот пример утрирован, но тем не менее наглядно показывает, чего стоит опасаться. Этот пример написан благодаря ответуDrOffset на форуме cyberforum на мой вопрос.
Добавить комментарий