5. Funkcje własne.

5.11. Przeciążanie funkcji: część 2

Stosowanie przeciążania funkcji wymaga od programisty wielkiej uwagi.


Przykład 1

Przeciążone funkcje można opisać w taki sposób, że dla kompilatora będą one mieć identyczne sygnatury, choć z punktu widzenia programisty opis ich parametrów się różni (kod 1).

Kod 1
#property strict

void OnStart()
  {
   int    a  = 1;
   double BB = 2.2;
   double v  = MyFunction(a, BB);

   Print("Wynik = ", DoubleToString(v));
  }

//--- 1)
double MyFunction(int f_1, double f_2)
  {
   double result = f_1 + f_2;
   return(result);
  }

//--- 2)
double MyFunction(int & f_1, double f_2)
  {
   double result = f_1 + f_2;
   return(result);
  }

Powyższy kod nie zostanie skompilowany, ponieważ te dwie funkcje mają taką samą sygnaturę - identyczne nazwy, i tu i tam pierwszy parametr f_1 ma typ int, a drugi f_2 ma typ double. Błąd, który wyświetli kompilator będzie wyglądał tak: ”ambiguous call to overloaded function with the same parameters”, tj. niejednoznaczne wywołanie funkcji przeciążonej o tych samych parametrach.

Rys. 1. Błąd kompilacji.


My oczywiście wiemy, że w drugiej funkcji parametr f_1 został zapisany używając & (ampersand) jako wskaźnik (zaznaczono na żółto), jednak w tym przypadku kompilatorowi bez róźnicy sposób przekazywaniach danych do funkcji: czy kopiowanie, czy przekazywanie przez wskaźnik, czy wskaźnik ze specyfikatorem const, czy przypisując domyślną wartość parametrowi. W poniższym kodzie 2 w skrócony sposób opisałem kilka funkcji, gdzie na żółto zaznaczyłem miejsca, które różnią je od funkcji 1.

Kod 2
#property strict

void OnStart()
  {
   int    a  = 1;
   double BB = 2.2;
   double v  = MyFunction(a, BB);

   Print("Wynik = ", DoubleToString(v));
  }

//--- 1)
double MyFunction(int f_1, double f_2)
   //...

//--- 2)
double MyFunction(int f_1, double & f_2)
   //...

//--- 3)
double MyFunction(const int & f_1, double f_2)
   //...

//--- 4)
double MyFunction(int f_1, const double & f_2)
   //...

//--- 5)
double MyFunction(int f_1, double f_2 = 5.5)
   //...

Dla funkcji 2, 3 i 4 kompilator wyświetli błąd jak w przypadku kodu 1 (rys. 1). Z kolei dla 5-ej funkcji kompilator pokaże: ”function already defined and has body”, tj. funkcja została już zdefiniowana i ma ciało. Jeśli w przypadku funkcji 2, 3 i 4 kompilator zobaczył jeszcze jakąś różnicę, to jednak przypisanie wartości domyślnej (double f_2 = 5.5) nie robi kompilatorowi żadnej różnicy. Dla niego funkcje 1 i 5 to ta sama funkcja zapisana dwa razy.


Przykład 2

Teraz opiszemy dwie funkcje (kod 3). Pierwsza będzie potrzebowała tylko 1 parametr: int f_1. Druga - 2 parametry: int f_1 oraz double f_2 i drugiemu parametrowi od razu przypiszemy domyślną wartość 5.5. Mogłoby się wydawać, że kompilator nie powinien mieć problemu z wyborem odpowiedniej funkcji, ale to już będzie zależało od tego, co zapiszemy w nagłówku wywoływanej funkcji MyFunction().

Kod 3
//--- Wariant 1
#property strict

void OnStart()
  {
   int    a  = 1;
   double BB = 2.2;
   double v  = MyFunction(a , BB);

   Print("Wynik = ", DoubleToString(v));
  }

//--- 1)
double MyFunction(int f_1)
  {
   double result = f_1 + 7.7;
   return(result);
  }

//--- 2)
double MyFunction(int f_1, double f_2 = 5.5)
  {
   double result = f_1 + f_2;
   return(result);
  }

Powyższy kod nie zawiera błędów. W nagłówku MyFunction() zapisaliśmy 2 argumenty a i BB i ta kombinacja idealnie pasuje do 2-je funkcji. W tym przypadku program normalnie się skompiluje i zmienna v otrzyma wartość 3.2 (1 + 2.2 = 3.2).

A czy uda się nam zaangażować do pracy 1-ą funkcję jeśli w nagłówku zapiszemy tylko jeden argument a (kod 4)?

Kod 4
//--- Wariant 2
#property strict

void OnStart()
  {
   int    a  = 1;
   double BB = 2.2;
   double v  = MyFunction( a );

   Print("Wynik = ", DoubleToString(v));
  }

//--- 1)
double MyFunction(int f_1)
  {
   double result = f_1 + 7.7;
   return(result);
  }

//--- 2)
double MyFunction(int f_1, double f_2 = 5.5)
  {
   double result = f_1 + f_2;
   return(result);
  }

Niestety w tym przypadku kompilator już nie będzie wiedział o jaką funkcję nam chodzi i wyświetli błąd: ”ambiguous call to overloaded function with the same parameters”, tj. niejednoznaczne wywołanie funkcji przeciążonej o tych samych parametrach. Z rozdziału 5.9. Domyślne wartości parametrów wiemy, że w przypadku jeśli parametr ma przypisaną wartość domyślną to w nagłówku wywoływanej funkcji odpowiedniego argumentu można nie pisać. Tu właśnie tak się stało, kompilator nie wiedział czy brak 2-go argumentu BB oznacza, że ma on zaangażować do pracy 1-ą funkcję, czy przymknąć na to oko i użyć 2-ej funkcji, gdzie f_2 ma wartość 5.5. W odróżnieniu od człowieka, kompilator nie znosi dwuznacznych sytuacji, dlatego od razu zacznie się kłócić i domagać się poprawienia kodu.

Jeśli zaś parametr f_2 nie będzie miał przypisanej wartości, to zapisując w nagłówku funkcji tylko zmienną a kompilator będzie miał jasną sytuację i zaangażuje 1-ą funkcję. Jeśli przekażemy a i BB to 2-ą funkcję.


Przykład 3

A co by się stało gdybyśmy w kodzie 4 w nagłówku MyFunction() zapisali tylko jedną zmienną typu double, np. BB (kod 5).

Kod 5
#property strict

void OnStart()
  {
   int    a  = 1;
   double BB = 2.2;
   double v  = MyFunction(BB);

   Print("Wynik = ", DoubleToString(v));
  }

//--- 1)
double MyFunction(int f_1)
  {
   double result = f_1 + 7.7;
   return(result);
  }

//--- 2)
double MyFunction(int f_1, double f_2)
  {
   double result = f_1 + f_2;
   return(result);
  }

W tym przypadku, kompilator znowu nie będzie miał łatwego zadania, którą z przeciążonych funkcji wybrać. Tu najlepiej będzie pasować pierwsza, ponieważ ma ona jeden parametr. Tylko, że w opisie funkcji ma on typ int, a funkcja otrzymała wartość typu double. W tym przypadku zadziała zasada ukrytej konwersji. Liczba typu double zostanie zamieniona na liczbę typu int, o czym zostaniemy ostrzeżeni przez kompilator (rys. 2).

Rys. 2. Ostrzeżenie kompilatora.


Nie jest to błędem kompilacji, dlatego program się skompiluje i można go będzie uruchomić w MetaTrader 4 (rys. 3).

Rys. 3. Wynik działania skryptu.


Widzimy wynik obliczenia 9.70000000. Wiemy już, że DoubleToString() konwertuje typ double na typ string i domyślnie wyświetla 8 cyfr po znaku dziesiętnym. Dlatego tu po kropce widzimy aż tyle zer.

Teraz pytanie, czemu wynik jest równy 9.7? Wiemy, że program wybrał 1-ą funkcję i liczba 2.2 z typu double została przekonwertowana na int, który jest typem liczby całkowitej. Z liczby 2.2 reszta dziesiętna zostanie usunięta i pozostanie samo 2. W 1-ej funkcji do 2 dodaje się 7.7 i liczba 9.7 zostanie zwrócona przez funkcję. Ten wynik zostanie przypisany zmiennej v i wyświetlony w dzienniku logów za pomocą Print().


Jak widać stosowanie przeciążania funkcji może nie być łatwe. Z drugiej strony jest to bardzo przydatna technika programowania, którą chciałem tu opisać abyś miał świadomość istnienia takiej możliwości. Zdaję sobie sprawę z tego, że łatwo jest pogubić się wśród tych wszystkich ograniczeń, dlatego jeśli jeszcze nie czujesz się na siłach aby stosować tę technikę to po prostu używaj unikalnych nazw funkcji własnych, bez przeciążania żadnej z nich i swojego mózgu.