Jeśli zdefiniowaliśmy klasę, a więc nowy typ danych (zmiennych), to często chcielibyśmy określić, w jaki sposób ma być dokonywana konwersja od obiektów innych typów do obiektów zdefiniowanej przez nas klasy. Z drugiej strony, chcielibyśmy czasem zdefiniować konwersje odwrotne: od obiektów naszej klasy do obiektów innych typów (wbudowanych, bibliotecznych lub też zdefiniowanych przez nas).
Przypuśćmy, że klasa A ma konstruktor, który może być wywołany z jednym argumentem typu B. A zatem albo jest to konstruktor jednoparametrowy z parametrem typu B, albo pierwszy parametr ma taki typ, a pozostałe parametry mają wartości domyślne. Typ B może być typem wbudowanym (double, int, ...), albo typem przez nas zdefiniowanym.
Załóżmy teraz, że użyjemy obiektu klasy B w kontekście, w którym wymagany jest obiekt typu A. Jeśli opisany wyżej konstruktor w klasie A istnieje, to dokonana zostanie konwersja B → A poprzez utworzenie nowego obiektu klasy A z dostarczeniem danego obiektu klasy B do konstruktora jako jedynego argumentu.
Na przykład, jeśli istnieje funkcja o parametrze typu A
void fun(A a) { ... }a jej wywołanie ma postać fun(4.25), to mamy do czynienia z niezgodnością typów, bo argument jest typu double. Błędu jednak nie będzie, jeśli klasa A ma konstruktor, który można wywołać z jednym argumentem typu double. Jeśli bowiem taki konstruktor konwertujący jest, to zostanie utworzony obiekt klasy A z użyciem tego konstruktora (do którego zostanie przesłana jako argument wartość 4.25). Tak utworzony obiekt zostanie następnie przesłany do funkcji fun.
A co będzie, jeśli tę samą funkcję wywołamy z argumentem całkowitym, na przykład ' fun(4)'? Takie wywołanie też będzie wtedy prawidłowe! Nie ma, co prawda, bezpośredniej konwersji int → A, bo nie ma w klasie A konstruktora pobierającego jeden argument typu int. Istnieje jednak standardowa konwersja int → double, a zdefiniowaną mamy, poprzez odpowiedni konstruktor, konwersję double → A. Zatem za pomocą takiej dwustopniowej konstrukcji można skonwertować wartość całkowitą do obiektu klasy A: najpierw utworzona zostanie tymczasowa zmienna typu double, a z niej obiekt klasy A. W sekwencji kilku konwersji prowadzących do celu takich konwersji pośrednich może być nawet więcej. Ważne jest jednak, że
Gdybyśmy bowiem dopuścili dowolne sekwencje konwersji, to możliwości byłoby tak wiele, że trudno byłoby w nich wszystkich zorientować się samemu programiście; w szczególności, trudno byłoby nawet przewidzieć, jakie typy takimi sekwencjami konwersji można połączyć, a jakie nie.
Zresztą, możliwość użycia w sekwencji nawet jednej konwersji zdefiniowanej przez użytkownika może być niewygodna. Jest tak wtedy, gdy potrzebujemy w naszej klasie konstruktora jednoparametrowego, ale wcale nie chcemy, aby był on wykorzystywany niejawnie do konwersji. Takie konwersje mogą się bowiem pojawić w najmniej spodziewanych miejscach, gdzie po prostu nie przewidzieliśmy, że będą przez kompilator wygenerowane. Dlatego istnieje specjalne słowo kluczowe, explicit, które może być użyte jako modyfikator konstruktora. Jeśli go użyjemy, to konstruktor taki nie będzie nigdy wykorzystany jako konstruktor konwertujący, a tylko wtedy, gdy służy swemu właściwemu celowi, to znaczy gdy jawnie tworzymy obiekty klasy.
1. #include <iostream> 2. using namespace std; 3. 4. struct Point { 5. int x, y; 6. Point(int x = 0, int y = 0) : x(x), y(y) { } 7. }; 8. 9. struct Segment { 10. Point A, B; 11. // explicit 12. Segment(Point A = Point(), Point B = Point()) 13. : A(A), B(B) 14. { } 15. }; 16. 17. void showPoint(Point A) { 18. cout << "Point[" << A.x << "," << A.y << "]"; 19. } 20. 21. void showSegment(Segment AB) { 22. cout << "Segment: "; 23. showPoint(AB.A); 24. cout << "--"; 25. showPoint(AB.B); 26. cout << endl; 27. } 28. 29. int main() { 30. int k = 7; 31. showPoint(k); 32. 33. cout << endl; 34. 35. Point A(1,1); 36. showSegment(A); 37. // showSegment(k); 38. }
Następnie zdefiniowane są dwie funkcje, showPoint i showSegment, których parametry są typu Point i Segment.
Przyjrzyjmy się teraz funkcji main. W linii 31 wywołujemy funkcję showPoint z argumentem typu int. Funkcji o takiej nazwie i takim typie parametru nie ma. Jest taka funkcja, tyle że z parametrem typu Point. Kompilator sprawdzi zatem, czy istnieje konwersja int → Point, to znaczy, czy istnieje konstruktor w klasie Point, który można wywołać z jednym argumentem typu int. Taki konstruktor istnieje, zatem konwersja zostanie dokonana: obiekt klasy Point na podstawie wartości całkowitej zostanie utworzony i wysłany do funkcji showPoint, jak o tym świadczy pierwsza linia wydruku:
Point[7,0] Segment: Point[1,1]--Point[0,0]W linii 35 tworzymy obiekt A klasy Point i posyłamy go do funkcji showSegment. Znów dokonana musi być konwersja, aby to wywołanie mogło być prawidłowe. Funkcja oczekuje argumentu typu Segment, zatem obiekt tej klasy zostanie utworzony z obiektu A za pomocą wywołania konstruktora klasy Segment z wartością A jako argumentem. Świadczy o tym druga linia wydruku.
Zauważmy, że wywołanie z wykomentowanej linii 37 byłoby nieprawidłowe. Wymagałoby konwersji dwustopniowej: najpierw int → Point, potem Point → Segment. A zatem użyte musiałyby być dwie konwersje definiowane w programie: to jest jednak niemożliwe.
Spróbujmy teraz uaktywnić wykomentowaną linię 11. Konstruktor klasy Segment jest teraz zdefiniowany z modyfikatorem explicit. Nie może zatem pełnić roli konstruktora konwertującego Point → Segment. Wywołanie z linii 36 staje się teraz nieprawidłowe, bo wymaga właśnie takiej konwersji. Taki program, z „odkomentowaną” linią 11, jest wobec tego błędny:
cpp> g++ -Wall -pedantic-errors convto.cpp convto.cpp: In function `int main()': convto.cpp:36: error: conversion from `Point' to non-scalar type `Segment' requested
Jako drugi przykład rozpatrzmy klasę
Modulo, której
użyliśmy już w programach
modsev.cpp
i
modsev1.cpp. Teraz zauważmy, jak konstruktor
konwertujący ułatwi nam przeciążenie operatora dodawania liczb
typu
Modulo.
1. #include <iostream> 2. using namespace std; 3. 4. class Modulo { 5. int numb; 6. public: 7. static int modul; 8. 9. Modulo() : numb(0) { } 10. 11. Modulo(int numb) : numb(numb%modul) { } 12. 13. friend Modulo operator+(Modulo,Modulo); 14. friend ostream& operator<<(ostream&,const Modulo&); 15. }; 16. int Modulo::modul = 7; 17. 18. Modulo operator+(Modulo m, Modulo n) { 19. return Modulo(m.numb + n.numb); 20. } 21. 22. ostream& operator<<(ostream& str, const Modulo& m) { 23. return str << m.numb; 24. } 25. 26. int main() { 27. 28. Modulo m(5), n(6), k; 29. 30. k = m + n; 31. cout << "m + n (mod " << Modulo::modul 32. << ") = " << k << endl; 33. 34. k = m + 6; 35. cout << "m + 6 (mod " << Modulo::modul 36. << ") = " << k << endl; 37. 38. k = 6 + m; 39. cout << "6 + m (mod " << Modulo::modul 40. << ") = " << k << endl; 41. }
m + n (mod 7) = 4 m + 6 (mod 7) = 4 6 + m (mod 7) = 4Ta ostatnia forma dodawania, liczba+obiekt, nie byłaby możliwa do zrealizowania przez przeciążenie operatora dodawania za pomocą metody, bo po lewej stronie mamy tu liczbę, a nie obiekt klasy. Dlaczego mogliśmy zdefiniować tylko jedną formę funkcji przeciążającej operator dodawania, tę z oboma parametrami typu Modulo? Było to możliwe dzięki konstruktorowi z linii 11, który jest konstruktorem konwertującym int → Modulo. Za jego pomocą argument całkowity zostanie przekonwertowany automatycznie do typu Modulo tam, gdzie konieczność takiej konwersji będzie wynikała z kontekstu, i to niezależnie od tego, czy liczba występuje po lewej, czy po prawej stronie operatora ' +'.
Opisanym sposobem możemy przekształcać obiekty pewnej klasy (w szczególności typu wbudowanego) na obiekty definiowanej przez nas klasy. Można również „nauczyć” kompilator operacji odwrotnej: konwertowania obiektów definiowanej przez nas klasy na obiekty innego typu (w szczególności typu wbudowanego). Jest to jedyne wyjście, gdy klasa docelowa, czyli ta, do której ma nastąpić konwersja, jest dla nas niedostępna i nie możemy w niej dodefiniować konstruktora konwertującego (bo, na przykład, typem docelowym jest typ wbudowany w ogóle nie będący klasą, albo klasa docelowa pochodzi z biblioteki, której nie możemy lub nie chcemy modyfikować).
W takiej sytuacji w definiowanej przez nas klasie definiujemy metodę konwertującą (ang. conversion method). Jest to bezparametrowa metoda o nazwie ' operator Typ', gdzie Typ jest nazwą typu (klasy) docelowego — może to być typ wbudowany, jak int czy double, a może też być to typ przez nas zdefiniowany.
Dla metod konwertujących, wyjątkowo, nie podaje się typu zwracanego, ale nie oznacza to, że metoda jest bezrezultatowa. Wartość zwracana musi mieć typ określony nazwą metody; musi zatem zawierać instrukcję return zwracającą wartość tego typu. Ponieważ jest to metoda, zawsze będzie działać na rzecz konkretnego obiektu: jej zadaniem jest „wyprodukowanie” obiektu odpowiedniego typu (do którego następuje konwersja) na podstawie obiektu, na rzecz którego działa.
Rozważmy przykład, podobny do tego z programu
convto.cpp:
1. #include <iostream> 2. #include <cmath> 3. using namespace std; 4. 5. struct Punkt { 6. double x, y; 7. Punkt(double x = 0, double y = 0) : x(x), y(y) { } 8. operator double() { 9. return sqrt(x*x+y*y); 10. } 11. }; 12. 13. struct Segment { 14. Punkt A, B; 15. Segment(Punkt A = Punkt(), Punkt B = Punkt()) 16. : A(A), B(B) 17. { } 18. operator Punkt() { 19. return Punkt( (A.x+B.x)/2, (A.y+B.y)/2 ); 20. } 21. }; 22. 23. void showPoint(Punkt A) { 24. cout << "Point[" << A.x << "," << A.y << "]"; 25. } 26. 27. void showSegment(Segment AB) { 28. cout << "Segment: "; 29. showPoint(AB.A); 30. cout << "--"; 31. showPoint(AB.B); 32. cout << endl; 33. } 34. 35. void showDouble(double d) { 36. cout << "Double " << d; 37. } 38. 39. int main() { 40. Punkt A(3,4); 41. showPoint(A); // 1 42. cout << endl; 43. showDouble(A); // 2 44. 45. cout << endl; 46. 47. Segment BC(Punkt(1,1),Punkt(3,3)); 48. showSegment(BC); // 3 49. showPoint(BC); // 4 50. 51. cout << endl; 52. }
W linii 41 wywołujemy funkcję showPoint z argumentem typu Point, a więc typu zadeklarowanego parametru funkcji. Zaraz potem wywołujemy z tym samym argumentem funkcję showDouble. Funkcja ta spodziewa się argumentu typu double. Zatem potrzebna jest konwersja Point → double. Ponieważ taką konwersję zdefiniowaliśmy, wywołanie jest prawidłowe: przed przekazaniem do funkcji obiekt A klasy Point zostanie przekonwertowany do typu double, o czym świadczy druga linia wydruku:
Point[3,4] Double 5 Segment: Point[1,1]--Point[3,3] Point[2,2]Podobnie będzie w linii 49. Do funkcji showPoint, która ma parametr typu Point, przesyłamy obiekt klasy Segment. Zostanie zatem użyta metoda konwertująca klasy Segment zdefiniowana w liniach 18-20 i określająca konwersję Segment → Point.
Zauważmy, że w drugim przypadku ten sam cel moglibyśmy osiągnąć definiując konstruktor przyjmujący jeden argument typu Segment w klasie Point. Nie było to jednak możliwe w przypadku poprzednim, gdyż nie da się dodefiniować konstruktora konwertującego w klasie double (przede wszystkim dlatego, że takiej klasy nie ma!).
Zauważmy na koniec, że metody konwertujące są dziedziczone i mogą być wirtualne — sens tego omówimy w jednym z następnych rozdziałów.
T.R. Werner, 21 lutego 2016; 20:17