Когда мы пишем программу, в которой описываем свой класс, и учим объекты описанного класса операциям и умениям преобразований легко попасть в ситуацию, где компилятор не сможет однозначно определить, что от него требуется.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #1
classMyClass{
public:
operatordouble(){return0.5;};//научили объекты этого класса преобразовываться к типу double
frienddoubleoperator+(constMyClass,constint){return5;}//научили к объекту этого класса прибавлять целое число
};
intmain()
{
MyClassm;
doubled=20;
m+d;//<--- Ошибка, компилятор колеблется между типами double и MyClass
}
Если в листинге #1 убрать одно любое умение класса, то код запустится, но вместе они порождают неоднозначность.
Для начала следует вспомнить, что дружественные функции не являются членами класса, это только настройка отношения класса к некоторой функции показанного вида. Можно это же воспроизвести и без дружественности:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #2
classMyClass{
public:
operatordouble(){return0.5;};//научили объекты этого класса преобразовываться к типу double
};
doubleoperator+(constMyClass,constint){return5;}//научили к объекту этого класса прибавлять целое число
intmain()
{
MyClassm;
doubled=20;
m+d;//<--- Ошибка, компилятор колеблется между типами double и MyClass
}
Почему это происходит? Потому что в операции сложения в получившейся ситуации возможны два исхода:
1. тип объекта m преобразуется к типу double и происходит (double + double). Это если сработал operator double
2. тип объекта m остается без изменений и происходит MyClass + double. Это если сработал operator+
На самом деле это легко воспроизвести:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Листинг #3
classMyClass{
public:
MyClass(){}
MyClass(int){}
operatordouble(){return0.5;}
};
voidfoo(MyClassx,int){}
voidfoo(double,double){}
intmain(){
MyClassx(9);
doubley;
foo1(x,y);//возникновение неоднозначности
}
Одним примером всё, конечно, не ограничивается. Возможно разное написание кода, где научивание классов может зародить неоднозначность, т. е. компилятор не сможет выбрать наиболее подходящий вариант из возможных:
C++
1
2
3
4
5
6
7
8
9
10
11
12
//Листинг #4
#include <iostream>
structMyInt{
operatorint(){return100;}//способность к преобразованию 1
operatordouble(){return100.5;}//способность к преобразованию 2
};
intmain(){
MyIntx;
std::cout<<x<<'\n';
}
Если убрать любую одну из выданных классу способностей по преобразованию, то код успешно запустится, но вместе эти способности не уживутся. Во время вывода на экран компилятор распознает возможность вывода на экран объекта как числа, но не распознает, какой тип мы хотим в конечном итоге увидеть, поэтому программа не компилируется.
Листинг #4 будет работать, если использовать явные преобразования типов. Коли уж так вышло, что компилятор колеблется, то можно ему подсказывать.
На самом деле неявные преобразования не всегда желательны, хотя кажутся очень удобными:
Функции неявного преобразования следует применять осторожно. Часто лучшим выбором будет функция, которая может вызываться только явно.
В C++ возможны несколько способов добиться того, чтобы механизм преобразования срабатывал только по прямой указке: заменить преобразующую функцию обычной функцией, использовать ключевое слово explicit
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Листинг #5.1 Делаем явные преобразования Visual Studio
#include <iostream>
structMyInt{
intconvert_to_int(){return100;}
doubleconvert_to_double(){return100.5;}
};
intmain(){
MyIntx;
//std:: cout << x << '\n'; //механизм преобразований не требуется, операции << для MyInt не определено, ошибка
std::cout<<x.convert_to_int()<<'\n';//ОК, провелия явное преобразование, использовав обычный метод и вывели int на экран
std::cout<<x.convert_to_double()<<'\n';//ОК, провелия явное преобразование, использовав обычный метод и вывели int на экран
}
В листинге #5.1 структура не научена чему-то полезному, просто объекты её класса возвращают либо целое, либо число с точкой, определённое мной. Это сделано, чтобы сократить код и чтобы вы смогли легче ориентироваться.
Начиная с С++11 можно запрещать неявность преобразований путём приписывания ключевого слова explicit к преобразующему элементу класса:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #5.2 Делаем явные преобразования C++11
#include <iostream>
usingstd::cout;
structMyInt{
explicitoperatorint(){return100;}
explicitoperatordouble(){return100.5;}
};
intmain(){
MyIntx;
//cout << x << '\n'; //механизм преобразований не требуется, операции << для MyInt не определено, ошибка
cout<<double(x)<<'\n';//ОК. явно указали необходимость преобразования.
cout<<int(x)<<'\n';//ОК. явно указали необходимость преобразования.
}
Поскольку в листинге #5.2 неявные преобразования запрещены, то при попытке вывода на экран в первом случае компилятор не задействует механизм преобразований, но если не задействовать механизм преобразований, то cout должен уметь выводить на экран объект нашего класса MyInt, чего он не умеет, потому для класса MyInt не определено операции >>, отсюда происходит ошибка. Пусть вас не сбивает с толку то, что я сейчас называю структуру классом: структуры то же, что классы, только доступ у структур по умолчанию открыт (public), а у классов закрыт (private), больше никаких отличий нет: всё, что справедливо для классов, справедливо для структур.
В идеале, чтобы избавиться от неявных преобразований, нужно указать explicit повсюду, откуда может произойти неявное преобразование: конструктор, преобразующая часть класса. Перекрыть лазейки, в общем.
Много случаев возможно, когда трудно заметить, что идёт не так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Листинг #6
#include <iostream>
usingstd::cout;
classMyClass{
intx;
public:
MyClass(){}
operatorint(){return5;}
};
intmain(){
intarr[20]={1,2,3,4,5,6,7,8,9,0};
MyClass temp;
intTemp=0;
cout<<arr[temp]<<'\n';//можно бы было ждать 1, но выводится 6
}
В листинге #6 из-за схожести названий легко перепутать одно с другим, а из-за обученности класса к преобразованию своих объектов к типу int объект класса стало возможно использовать в качестве индекса массива: происходит неявное преобразование типов в показанном коде. Но то, что хотел написавший похожий код программист, могло быть другим. Поскольку код работает и ошибок не возникает, то словить ошибку может быть сложно. Это сейчас код маленький и всё кажется очевидным, но если код большой и это какой-нибудь многофайловый проект, то такие ошибки легко доставляют головной боли и на их поиски может уходить много времени, которое, возможно, могло бы быть потрачено с пользой. Конечно, пример утрирован, но он, как обычно, максимально упрощён, чтобы попытаться донести суть потенциальных проблем от неявных преобразований типов. Да и потом от неявности вы уже могли успеть пострадать, если решали математические примеры и double молча усекаясь до int помогал давать неожидаемые (неопытному новичку) результаты. Суть-то та же. Неявным преобразованиям следует предпочитать явные.
cout<<MyClass(20)+10;//могло бы ожидаться что будет 30, но ответ 15
}
В листинге #7 срабатывает конвертирующий конструктор и в начале в x приходит значение 20, но конвертирующим конструктором тип int преобразуется к типу MyClass, а не наоборот. А когда наступает очередь выполнения операции +, компилятор видит два типа MyClass + int. Поскольку MyClass научен приводить свои объекты к типу int неявно, происходит неявное приведение, типы выравниваются и происходит сложение. Но из-за того, что в обученности приведения мы в настоящем случае возвращаем 5, то в конечном итоге путём цепочки неявных приведений всё сводится к 5 + 10, что и получается результатом 15. По мере написания вами ваших кодов следует быть очень аккуратными с задаваемой вами неявностью преобразований, и если нет жёсткого ограничения или точного понимания, что оно действительно надо, избегайте неявности, иначе можно ловить сложнообнаруживаемую проблему.
Добавить комментарий