11.11 Przeciążanie funkcji

Funkcje można w C++ (ale nie w C) przeciążać. Oznacza to sytuację, gdy w tym samym zakresie są widoczne deklaracje/definicje kilku funkcji o tej samej nazwie. Oczywiście, aby na etapie kompilacji było wiadomo, o wywołanie której funkcji nam chodzi, wszystkie wersje funkcji muszą się wystarczająco różnić. Co to znaczy wystarczająco? Generalnie, muszą się różnić na tyle, aby można było jednoznacznie wybrać jedną z nich na podstawie wywołania.

Warunkiem koniecznym, choć niewystarczającym jest, aby funkcje o tej samej nazwie różniły się sygnaturą.

Do sygnatury funkcji należy jej nazwa oraz liczba i typ parametrów nie licząc tych z wartościami domyślnymi. Typ funkcji, czyli typ wartości zwracanej, zwykle do sygnatury nie jest zaliczany.

Tak więc na przykład

       int    fun(double x, int k =0);
       double fun(double z);
to dwie deklaracje różnych funkcji, ale o takiej samej sygnaturze, a mianowicie o sygnaturze fun(double). I rzeczywiście, wywołanie fun(1.5) byłoby najzupełniej legalnym i nie wymagającym żadnej niejawnej konwersji wywołaniem zarówno pierwszej, jak i drugiej z tych funkcji. Takie przeciążenie jest zatem nielegalne.

Natomiast

       double fun(int);
       double fun(unsigned);
to deklaracje funkcji różniących się sygnaturą. Wywołanie fun(15) jest wywołaniem pierwszej z nich, bo '15' jest literałem wartości typu int i do przekształcenia tej wartości do typu unsigned potrzebna byłaby konwersja. Zatem takie przeciążenie jest prawidłowe.

Z drugiej strony, różne sygnatury nie są jeszcze warunkiem dostatecznym na legalność przeciążenia. Widzimy to na przykładzie funkcji

       void fun(int i);
       void fun(int& i);
które mają różną sygnaturę, ale wywołanie fun(k), gdzie k jest typu int, może być traktowane jako wywołanie zarówno pierwszej, jak i drugiej z nich. Zatem takie przeciążenie byłoby nieprawidłowe. Podobnie nieprawidłowe byłoby przeciążenie
       void fun(int tab[]);
       void fun(int * p);
lub
       void fun(tab[3][3]);
       void fun(tab[5][3]);
bo pierwszy wymiar tablicy wielowymiarowej nie ma znaczenia do określenia typu (jest i tak pomijany w tego rodzaju deklaracji/definicji). Natomiast
       void fun(tab[3][3]);
       void fun(tab[3][5]);
prawidłowo deklaruje dwie przeciążone funkcje fun, gdyż tablice wielowymiarowe różniące się wymiarem innym niż pierwszy różnych typów i pomiędzy tymi typami nie ma niejawnej konwersji.

Argument typu T może być użyty przy wywołaniu funkcji z parametrem typu T, const Tvolatile T, więc funkcje przeciążone nie mogą się różnić tylko typem takiego parametru. Zatem

       int fun(int k);
       int fun(const int k);
byłoby nielegalne.

Natomiast typy parametrów T*, volatile T*const T* (i analogicznie T&, volatile T&const T&) są wystarczająco różne: patrząc na wywołanie funkcji kompilator może stwierdzić, czy użyta tam zmienna była ustalona lub ulotna czy nie; przeciążenie

       int fun(int& k);
       int fun(const int& k);
jest zatem prawidłowe.

Może się jednak zdarzyć, że to samo wywołanie funkcji pasuje do kilku jej przeciążonych wersji po ewentualnym dokonaniu dozwolonych konwersji. Jak rozstrzygnąć, która z tych funkcji będzie wywołana?

Proces poszukiwania takiej funkcji przebiega etapami i kończy się, gdy zostanie znalezione dopasowanie funkcji do wywołania. Tak więc sprawdzane są kolejno różne typy (stopnie) dopasowania:

Dopasowanie dokładne. Wszystkie typy argumentów są identyczne jak typy odpowiednich parametrów.

Dopasowanie po konwersji trywialnej. Do pełnego dopasowania wystarczy konwersja trywialna argumentu (ang. minor conversion). Konwersje trywialne to (T oznacza pewien typ, fun funkcję —  o wskaźnikach funkcyjnych powiemy w rozdziale im poświęconym ):

Tabela: Konwersje trywialne
T T&
T& T
T[] T*
fun() (*fun)()
T const T
T volatile T
T* const T*
T* volatile T*

Dopasowanie po promocji. Do uzyskania dopasowania wystarczą standardowe promocje całościowe lub zmiennopozycyjne — patrz rozdział o konwersjach standardowych — na przykład char int.

Dopasowanie po innej konwersji niejawnej. Do uzyskania dopasowania wystarczą standardowe konwersje nie będące promocjami całościowymi lub zmiennopozycyjnymi — patrz rozdział o konwersjach standardowych — na przykład int double lub odwrotnie.

Dopasowanie po konwersji zdefiniowanej przez użytkownika. Do uzyskania dopasowania potrzebne są konwersje zdefiniowane przez użytkownika — jak takie konwersje definiować, omówimy w rozdziale o konwersjach .

Dopasowanie do funkcji o zmiennej liczbie parametrów. Ostatnia, rozpaczliwa próba dopasowania może być podjęta, jeśli wywołanie może pasować do funkcji o nieustalonej liczbie parametrów.

Znajdowanie dopasowania to skomplikowany proces, którego wynik nie zawsze jest intuicyjny. Rozpatrzmy program:


P73: match.cpp     Dopasowywanie funkcji przeciążonych do wywołania

      1.  #include <iostream>
      2.  #include <string>
      3.  using namespace std;
      4.  
      5.  string fun1(   int) { return "\'int\'\n";    }
      6.  string fun1(  char) { return "\'char\'\n";   }
      7.  string fun1(double) { return "\'double\'\n"; }
      8.  
      9.  string fun2( short) { return "\'short\'\n";  }
     10.  string fun2(double) { return "\'double\'\n"; }
     11.  
     12.  int main() {
     13.      int    kin =   0;
     14.      char   kch = '\0';
     15.      float  kfl =   0;
     16.      double kdo =   0;
     17.  
     18.      cout << "fun1(   int) -> " << fun1(kin);
     19.      cout << "fun1(  char) -> " << fun1(kch);
     20.      cout << "fun1( float) -> " << fun1(kfl);  
     21.      cout << "fun1(double) -> " << fun1(kdo);
     22.  
     23.      cout << "fun2( float) -> " << fun2(kfl);  
     24.    //cout << "fun2(  char) -> " << fun2(kch);
     25.    //cout << "fun2(   int) -> " << fun2(kin);
     26.  }

Definiujemy trzy funkcje o tej samej nazwie fun1. Funkcje zwracają identyfikujący je napis. Wywołujemy je w funkcji main z argumentami różnych typów. Z wydruku

    fun1(   int) -> 'int'
    fun1(  char) -> 'char'
    fun1( float) -> 'double'
    fun1(double) -> 'double'
    fun2( float) -> 'double'
widzimy, że zawsze wywołana jest ta funkcja, której parametr dokładnie pasuje do typu argumentu. Wyjątkiem jest wywołanie z argumentem typu float z linii  — tu jednak nie dziwi nas, że wybrana została funkcja z parametrem typu double: wystarczyła tu jedna standardowa promocja zmiennopozycyjna (konwersja float int też jest standardową konwersją, ale nie standardową promocją).

Ciekawsze są natomiast wywołania funkcji fun2. Istnieją jej dwie wersje: z argumentem typu shortdouble. Nie ma kłopotów z wywołaniem funkcji z argumentem typu float () — konwersja float double jest standardową promocją. Ale zakomentowane wywołanie fun2(kch), gdzie kch jest typu char jest błędne! Argument jest tu typu char, więc wydawałoby się, że ma „bliżej” do typu short niż do typu double. Standardową promocją całościową jest jednak promocja char int; promocja char short nią nie jest i wobec tego jest uznana za „dopasowanie po innej konwersji niejawnej", a więc takie samo jak char double. Zatem kompilator zgłosi błąd uznając obie wersje funkcji fun2 za tak samo dobre (albo tak samo złe).

Podobnie jest dla wykomentowanego wywołania tej samej funkcji z argumentem typu int (ostatnia linia). Wydaje się, że lepsza jest promocja int double, będąca konwersją rozszerzającą, niż przekształcenie int short, które jest konwersją zawężającą, a więc potencjalnie „gubiącą” informację. A jednak są one tak samo dobre/złe: ponieważ konwersja pierwsza jest, co prawda, promocją, ale nie całościową, obie znowu wpadają do tej samej kategorii „dopasowanie po innej konwersji niejawnej".

Oczywiście stosowanie takich nieczytelnych przeciążeń może prowadzić do trudno wykrywalnych błędów i generalnie powinniśmy ich unikać.

T.R. Werner, 25 lutego 2017; 22:31