Jedna nazwa, wiele wersji
Często potrzebujesz tej samej operacji dla różnych rodzajów danych: wypisać int, wypisać string, wypisać double. W niektórych językach wymyśla się wtedy printInt, printString, printDouble. C++ pozwala nadać im wszystkim tę samą nazwę i rozróżnia je po parametrach. To właśnie przeciążanie funkcji.
Reguła jest prosta: kilka funkcji może dzielić nazwę, o ile ich listy parametrów się różnią: liczbą parametrów, ich typami albo kolejnością. Kompilator patrzy na argumenty w każdym miejscu wywołania i wybiera za ciebie pasującą wersję.
Trzy funkcje, jedna nazwa. Każde wywołanie trafia do wersji, której typ parametru pasuje do argumentu. Dzięki temu std::cout << x działa tak samo dla liczb całkowitych, zmiennoprzecinkowych i napisów: operator<< jest przeciążony wielokrotnie.
Co liczy się jako inne przeciążenie
Przeciążenie wyróżnia wyłącznie lista parametrów. Możesz zmieniać:
int area(int side); // 1 parametr
int area(int width, int height); // 2 parametry -> inne
double area(double r); // inny typ -> inne
void log(string msg, int level); // kolejność ma znaczenie...
void log(int level, string msg); // ...więc to też jest inne
Każde z nich to legalne, odrębne przeciążenie. Kompilator tworzy zbiór kandydatów ze wszystkich funkcji o nazwie area, a potem dopasowuje je po liczbie i typie argumentów.
Sam typ zwracany nie wystarczy
Oto pułapka, na której potyka się prawie każdy: nie można przeciążać po typie zwracanym. Wartość zwracana nie bierze udziału w wyborze przeciążenia, bo kompilator decyduje, którą funkcję wywołać, na podstawie argumentów, zanim w ogóle spojrzy na to, co wraca.
int convert(double x); // OK
double convert(double x); // BŁĄD: redefinicja, różni się tylko typ zwracany
To się nie skompiluje. Jeśli listy parametrów są identyczne, z punktu widzenia przeciążania obie deklaracje to ta sama funkcja i dostajesz błąd redefinicji. Aby rozgałęzić zachowanie po typie wyniku, zmień parametr (albo użyj szablonów lub rzutowania typu w miejscu wywołania).
Jak rozstrzyganie przeciążeń wybiera zwycięzcę
Gdy wywołujesz funkcję, kompilator ocenia każde możliwe przeciążenie i wybiera najlepsze dopasowanie. W przybliżeniu preferuje, w kolejności:
- Dokładne dopasowanie (bez żadnej konwersji).
- Promocję (np.
charlubshort->int,float->double). - Konwersję standardową (np.
int->double,double->int, wskaźnik na klasę bazową).
Jeśli dokładnie jedno przeciążenie jest ściśle lepsze od wszystkich pozostałych, ono wygrywa. Zobacz, jak dokładne dopasowanie pokonuje konwersję:
'A' to char, ale promocja do int ma wyższą rangę niż konwersja do double, więc wywołane zostaje przeciążenie int. Dzięki tym regułom rangowania rozstrzyganie przeciążeń zwykle "po prostu robi, co trzeba", a od czasu do czasu potrafi zaskoczyć.
Pułapka niejednoznaczności
Jeśli dwa przeciążenia są równie dobre, czyli żadne nie jest ściśle lepsze, kompilator odmawia zgadywania i zgłasza niejednoznaczne wywołanie (ambiguous call). Podręcznikowy przypadek to dwa przeciążenia, z których każde wymaga konwersji tej samej rangi:
void f(int x);
void f(double x);
f(0L); // BŁĄD: niejednoznaczne, long -> int i long -> double to konwersje tej samej rangi
Ani int, ani double nie jest dokładnym dopasowaniem dla long, a obie konwersje mają tę samą rangę, więc wywołanie jest niejednoznaczne. Masz dwa czyste rozwiązania:
Pokrewna niespodzianka: przekazywanie literału napisowego. void g(const string&) i void g(bool) oba zgłoszą się do obsługi g("hi"), a bool może wygrać, bo const char* konwertuje się na bool (niepusty wskaźnik -> true) w mniejszej liczbie kroków niż budowa std::string. Jeśli kiedyś zobaczysz, że literał napisowy tajemniczo wywołuje twoje przeciążenie bool, to właśnie dlatego: dodaj przeciążenie const char* albo const string&, aby przejęło dokładne dopasowanie.
Przeciążanie i argumenty domyślne źle się łączą
Argumenty domyślne nie zastępują przeciążania, a łączenie jednego z drugim tworzy niejednoznaczność. Każde z nich może obsłużyć to samo wywołanie, więc kompilator nie potrafi wybrać:
void connect(string host, int port = 8080); // można wywołać z 1 argumentem
void connect(string host); // też można wywołać z 1 argumentem
connect("localhost"); // BŁĄD: niejednoznaczne, obie pasują do jednego argumentu
Dla danego kształtu wywołania wybierz jedno podejście. Używaj argumentów domyślnych, gdy zachowanie jest identyczne, a chodzi ci tylko o opcjonalne parametry; używaj przeciążania, gdy różne listy argumentów mają uruchamiać naprawdę różny kod. Pomieszanie ich tak, że dwie sygnatury kolidują przy tej samej liczbie argumentów, to gwarantowany błąd niejednoznaczności.
Jeszcze jedno rozróżnienie warto utrwalić: przeciążanie to nie nadpisywanie. Przeciążanie rozstrzyga się w czasie kompilacji spośród funkcji w tym samym zasięgu o tej samej nazwie, ale różnych parametrach. Nadpisywanie (overriding) zastępuje funkcję virtual w klasie pochodnej w czasie działania i wymaga tej samej sygnatury: to temat na później, przy funkcjach wirtualnych.
Dalej: lambdy
Przeciążanie daje jednej nazwie kilka typowanych implementacji wybieranych w czasie kompilacji. Czasem jednak w ogóle nie chcesz nazwanej funkcji: potrzebujesz malutkiej, jednorazowej funkcji zdefiniowanej dokładnie tam, gdzie jej używasz, często po to, by przekazać ją do algorytmu takiego jak sort. Tym właśnie są lambdy: anonimowe funkcje, które piszesz w miejscu, które przechwytują otaczające zmienne i które przekazujesz dalej w jednym wyrażeniu. Następnie zobaczysz, jak je pisać i kiedy wygrywają z pełną, nazwaną funkcją.
Najczęściej zadawane pytania
Czym jest przeciążanie funkcji w C++?
Przeciążanie funkcji pozwala zdefiniować wiele funkcji o tej samej nazwie, o ile ich listy parametrów się różnią (liczbą, typem lub kolejnością). Kompilator wybiera, którą wywołać, na podstawie przekazanych argumentów, więc print(42) i print("hi") mogą wywołać dwie różne funkcje print.
Czy dwie funkcje w C++ mogą różnić się tylko typem zwracanym?
Nie. Przeciążenia muszą różnić się listą parametrów. int f(int) i double f(int) to błąd kompilacji: typ zwracany nie należy do sygnatury używanej przy rozstrzyganiu przeciążeń, bo kompilator wybiera przeciążenie na podstawie argumentów w miejscu wywołania, zanim wartość zwracana zostanie w ogóle użyta.
Co powoduje błąd "ambiguous call" przy funkcjach przeciążonych?
Pojawia się, gdy dwa przeciążenia są równie dobrym dopasowaniem i kompilator nie może żadnego preferować. Klasyczny przypadek to f(int) i f(double) wywołane jako f(0L) (czyli z long), gdy oba wymagają konwersji tej samej rangi. Rozwiązaniem jest dodanie przeciążenia z dokładnym dopasowaniem albo rzutowanie argumentu na wybrany typ.