Niejawne przekształcenia typów

tech • 763 słowa • 4 minuty czytania

Konwersje typów zdefiniowanych przez użytkownika, czyli klas, są możliwe poprzez zdefiniowanie odpowiednich metod. Są nimi konstruktory konwersji dokonujące przekształcania z innych typów do naszego zdefiniowanego typu i operatory konwersji1 służące do przekształcania z typu zdefiniowanego przez użytkownika na inne typy.

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

Operator i konstruktor konwersji nie zmienia typu istniejącego obiektu lecz tworzy nowy obiekt int tam, gdzie jest potrzebny. Wszędzie, gdzie dopuszcza się 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 double w klasie X, wyrażenie to nadal byłoby poprawne. Zostałaby użyta niejawna konwersja typów 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 zmienne zostaną zainicjowane przez wywołanie konstruktora X(int).

Pierwszy sposób jest zwykłym użyciem konstruktora, natomiast drugi sposób jest używany 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 zapobiegające takiej konwersji 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 niejawną konwersję, czyli nie będzie można użyć inicjacji przy użyciu operatora = 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).

Niejawne konwersje typów prowadzą czasem do błędów trudnych do wykrycia lub niejednoznaczności.

Rozważmy taki przykład, mamy dwie klasy X i Y, których to konstruktory przyjmują zmienną n tego samego typu - int oraz funkcję f przeciążoną dla każdej z tych klas:

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

class Y {
public:
	Y(int n);
};

void f(X x);
void f(Y y);

Przy próbie wywołaniu funkcji f z argumentem int wystąpią niejednoznaczności:

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

Kompilator nie będzie wiedział, który konstruktor powinien zostać użyty przy wywołaniu funkcji f, co zasygnalizuje odpowiednim błędem. Problem ten zniknie po zadeklarowaniu choćby jednego z konstruktorów jako explicit.

Używanie jawnych konwersji (explicit) jest bardzo wskazane. Szkoda, że nie jest to domyślne ustawiane przez kompilator, jak choćby specyfikator auto dla zmiennych lokalnych. Wtedy można by dorzucić implicit do zmiany takiego standardowego zachowania. Taki odwrotny mechanizm pozwoliłoby zapobiec wielu potencjalnym błędom.

Może się czasami zdarzyć, że jest możliwe skonstruowanie żądanego obiektu poprzez wielokrotne używanie konstruktorów 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 funkcję:

class Z {
public:
	Z(X x);
};

void g(Z z);

A nastepnie wywołali funkcję g:

g(123);				// #1
g(X(123));			// #2
g(Z(123));			// #3
g(Z(X(123)));		// #4

Pierwszy przypadek (#1) jest niepoprawny, bo wymaga 2 poziomów niejawnej konwersji int -> X -> Z. Musimy jawnie specyfikować wywołanie konstruktora X(int) (#2) lub niejawnie przez wywołanie konstruktora Z z argumentem z jakiego można stworzyć obiekt typu X (#3). Ostatecznie jawnie wywołując wszystkie niezbędne konstruktory konwersji (#4).

Wracając do pierwszego przykładu z tej notki. Tam następowałaby niejawna standardowa konwersja z int na double, gdyby operator double nie był zdefiniowany. Konwersje standardowe można stosować do argumentu konwersji zdefiniowanych przez użytkownika, jak i do wyniku takich konwersji. Natomiast konwersje zdefiniowane przez użytkownika są wybierane na podstawie typu zmiennej, którą się inicjuje lub do której się przypisuje.

Dlatego poniższe wyrażenie w tamtych okolicznościach jest niejednoznaczne:

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

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 ze zdefiniowanych tam operatorów konwersji nie byłby dostępny to problem ten w ogóle by nie istniał.

Można byłoby tutaj przedstawić wiele przykładów 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++”.


Przypisy

  1. Często są też zwane funkcjami konwersji, bądź operatorami przekształcania lub rzutowania. ↩︎

Komentarze (0)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/