Niejawne przekształcenia typów

Konwersje typów zdefiniowanych przez użytkownika, czyli klas jest możliwa poprzez zdefiniowanie odpowiednich konstruktorów, aby umożliwić przekształcanie z jakiegoś innego typu na nasz typ  lub odpowiednich operatorów przekształcania (czasem zwanych operatorami konwersji lub rzutowania) do przekształcania naszego zdefiniowanego typu w inny typ.

class X {
public:
	X(int n);
	operator int();
	operator double();
};

Operator konwersji i konstruktor nie zmienia typu istniejącego obiektu, lecz tworzy nowy obiekt int, tam gdzie jest potrzebny, wszędzie, gdzie dopuszcza sie X, można użyć int i odwrotnie.

X x = 123;		// X(123)
int i = x;		// operator int()
double d = x;	// operator double()

W trzecim przypadku, gdyby nie był zadeklarowany operator konwersji do typu double w klasie X, wyrażenie to nadal byłoby poprawne, zostałaby użyta niejawna konwersja typow wbudowanych, z int do double w sposób X -> int -> double.

Deklarowanie funkcji konwersji do innego typu jest żądaniem niejawnego zastosowania funkcji przekazującej lub zwracającej dany typ, czyli niejawnej konwersji do danego typu.

Istnieją dwa sposoby inicjowania zmiennej typu X:

X x1 = B(123);
X x2 = 123;

W obu przypadkach zmiennie zostaną zainicjowane przez wywołanie konstruktora X(int).

Pierwszy sposób jest zwykłym wywołaniem konstruktora, natomiast drugi sposób, jest używany  tylko dla „backward compatibility” i wygody, niejawnie wywołuje on konstruktor klasy X jako narzędzie do konwersji z typu int do X. Warto zwrócić uwagę, że nie zostanie tutaj użyty operator przypisania = tylko konstruktor, bo następuje inicjacja, a nie przypisanie!

Ponieważ niejawne stosowanie konwersji zdefiniowanych przez konstruktory bywa zdradliwe i czasem bardzo niepożądane, wprowadzono do języka C++ słowo kluczowe explicit, które zapobiega niejawnej konwersji typów przez konstruktor.

class X {
public:
	explicit X(int n);
	operator int();
	operator double();
};

Zadeklarowanie konstruktora jedno argumentowego (lub wielo z domyślnymi wartościami poza pierwszym) jako explicit nie pozwoli na nie jawną konwersję, czyli nie będzie można użyć inicjacji przy użyciu = i argumentu konstruktora, trzeba będzie specyfikować pełnoprawne wywołanie konstruktora.

X x1 = B(123);	// ok
X x2 = 123;		// blad!

Sytuacje użycia explicit są przydatne przy zapobieganiu niekorzystnych wywołań konstruktorów oraz tworzenia tymczasowych obiektów na podstawie argumentów konstruktora (przy stałych referencjach).

Prowadzi to czasem do błędów trudnych do wykrycia lub niejednoznaczności.

Rozważmy przykład, mamy dwie klasy których konstruktory przyjmują zmienna tego samego typu oraz funkcje f przeciążoną dla każdej klasy:

class X {
public:
	X(int n);
};
 
class Y {
public:
	Y(int n);
};
 
void f(X x);
void f(Y y);

Przy wołaniu funkcji f z argumentem int:

f(123);	// blad! f(X) czy f(Y)?

wystąpią niejednoznaczności, kompilator nie będzie wiedział co ma zrobić, który konstruktor powinien zostać użyty przy wywołaniu funkcji f, co zasygnalizuje odpowiednim błędem.

Problem zniknie po zadeklarowaniu jednego z konstruktorów jako explicit.

Używanie explicit jest bardzo wskazane, szkoda ze nie jest to domyślne ustawiane przez kompilator jak choćby specyfikator auto dla zmiennych. Wtedy można by dorzucić implicit do zmiany standardowego zachowania.

Wracając do niejednoznaczności i innych problemów przy niejawnym przekształcaniu typów.

Może się zdążyć czasami, że jest możliwe skonstruowanie żądanego obiektu poprzez wielokrotne używanie konstruktora lub operatorów konwersji, ale trzeba to robić jawnie, ponieważ tylko jeden poziom niejawnej konwersji zdefiniowanej przez użytkownika jest dozwolony.

Gdybyśmy do powyższego przykładu dodali jeszcze jeden obiekt i funkcje:

class Z {
public:
	Z(X x);
};
 
void g(Z z);

I wywołali funkcję g:

g(123);
g(X(123));
g(Z(123));
g(Z(X(123)));

Pierwszy przypadek jest niepoprawny, bo wymaga 2 poziomów niejawnej konwersji int -> X -> Z.
Musimy jawnie specyfikować wywołanie konstruktora X(int) lub niejawnie przez wywolanie konstruktora Z z argumentem z jakiego można stworzyc obiekt typu X ;)
Ostatnie przypadek jawnie wywołuje wszystkie niezbędne konstruktory konwersji.

Wróćmy jeszcze na chwilę do pierwszego przykładu w tej notce. Tam następowałaby niejawna standardowa konwersja z int na double, kiedy operator double nie byłby zdefiniowany. Konwersje standardowe można stosować do argumentu konwersji zdefiniowanych przez użytkownika jak i do wyniku takich konwersji. Przez co kompilator niejawnie tam dokonywał takiego przekształcenia.

Natomiast konwersje zdefiniowane przez użytkownika są wybierane na podstawie typu zmiennej, która się inicjuje lub na którą się przypisuje. Dlatego wyrażenie:

float f = x;	// blad! float(x.operator int()) czy float(x.operator double())?

w tamtych okolicznosciach jest niejednoznaczne, bo nie jest zdefiniowana operacja konwersji X na float i kompilator nie wie jaki rodzaj niejawnej konwersji typów wbudowanych zastosować, czy z int na float, czy z double na float. Jeśli jeden z operatorów konwersji nie byłby zdefiniowany, problem nie istniałby.

Można by tutaj wiele przykładów przedstawić i pułapek na jakie jesteśmy narażeni przy niejawnej konwersji typów. Ciekawskich zachęcam do przejrzenia kilku rozdziałów w książce Stroustrupa „Język C++”.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *