W poprzedniej lekcji (11.2 -- Różnicowanie przeciążenia funkcji), omówiliśmy, które atrybuty funkcji służą do odróżniania przeciążonych funkcji od siebie. Jeśli przeciążona funkcja nie zostanie odpowiednio odróżniona od innych przeciążeń o tej samej nazwie, kompilator zgłosi błąd kompilacji.
Jednak posiadanie zestawu zróżnicowanych przeciążonych funkcji to tylko połowa obrazu. Kiedy wykonywane jest jakiekolwiek wywołanie funkcji, kompilator musi także upewnić się, że można znaleźć pasującą deklarację funkcji.
W przypadku funkcji nieprzeciążonych (funkcje o unikalnych nazwach) tylko jedna funkcja może potencjalnie pasować do wywołania funkcji. Ta funkcja albo pasuje (lub można ją dopasować po zastosowaniu konwersji typów), albo nie (co powoduje błąd kompilacji). W przypadku przeciążonych funkcji może istnieć wiele funkcji, które mogą potencjalnie pasować do wywołania funkcji. Ponieważ wywołanie funkcji może rozwiązać tylko jeden z nich, kompilator musi określić, która przeciążona funkcja jest najlepiej dopasowana. Proces dopasowywania wywołań funkcji do określonej przeciążonej funkcji nazywa się rozwiązywaniem przeciążenia.
W prostych przypadkach, gdy typ argumentów funkcji i typ parametrów funkcji są dokładnie zgodne, jest to (zwykle) proste:
#include <iostream>
void print(int x)
{
std::cout << x << '\n';
}
void print(double d)
{
std::cout << d << '\n';
}
int main()
{
print(5); // 5 is an int, so this matches print(int)
print(6.7); // 6.7 is a double, so this matches print(double)
return 0;
}Ale co się dzieje w przypadkach, gdy typy argumentów w wywołaniu funkcji nie odpowiadają dokładnie typom parametrów w którejkolwiek z przeciążonych funkcji? Na przykład:
#include <iostream>
void print(int x)
{
std::cout << x << '\n';
}
void print(double d)
{
std::cout << d << '\n';
}
int main()
{
print('a'); // char does not match int or double, so what happens?
print(5L); // long does not match int or double, so what happens?
return 0;
}To, że nie ma tutaj dokładnego dopasowania, nie oznacza, że nie można znaleźć dopasowania — w końcu a char lub long można niejawnie przekonwertować na typ int lub a double. Ale jaką konwersję najlepiej wykonać w każdym przypadku?
W tej lekcji przyjrzymy się, jak kompilator dopasowuje wywołanie danej funkcji do określonej przeciążonej funkcji.
Rozwiązywanie przeciążonych wywołań funkcji
Gdy wywoływana jest funkcja do przeciążonej funkcji, kompilator przechodzi przez sekwencję reguł, aby określić, która (jeśli w ogóle) z przeciążonych funkcji najlepiej pasuje (omówimy te kroki w następnej sekcji poniżej).
Na każdym kroku kompilator stosuje kilka różnych konwersji typów do argumentów w wywołaniu funkcji. Dla każdej zastosowanej konwersji kompilator sprawdza, czy którakolwiek z przeciążonych funkcji jest teraz zgodna. Po zastosowaniu wszystkich konwersji różnych typów i sprawdzeniu dopasowań, krok jest zakończony. Wynikiem będzie jeden z trzech możliwych wyników:
- Nie znaleziono pasujących funkcji. Kompilator przechodzi do następnego kroku w sekwencji.
- Znaleziono pojedynczą pasującą funkcję. Ta funkcja jest uważana za najlepiej dopasowaną. Proces dopasowywania został zakończony, a kolejne kroki nie są wykonywane.
- Znaleziono więcej niż jedną pasującą funkcję. Kompilator wyświetli niejednoznaczny błąd kompilacji dopasowania. Omówimy ten przypadek za chwilę.
Jeśli kompilator dotrze do końca całej sekwencji bez znalezienia dopasowania, wygeneruje błąd kompilacji informujący, że dla wywołania funkcji nie można znaleźć pasującej przeciążonej funkcji.
Sekwencja dopasowania argumentów
Krok 1) Kompilator próbuje znaleźć dokładne dopasowanie. Dzieje się to w dwóch fazach. Najpierw kompilator sprawdzi, czy istnieje przeciążona funkcja, w której typ argumentów w wywołaniu funkcji jest dokładnie zgodny z typem parametrów w przeciążonych funkcjach. Na przykład:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo(0); // exact match with foo(int)
foo(3.4); // exact match with foo(double)
return 0;
}Ponieważ 0 w wywołaniu funkcji foo(0) jest int, kompilator sprawdzi, czy zadeklarowano foo(int) przeciążenie. Ponieważ tak jest, kompilator określa, że foo(int) jest dokładnym dopasowaniem.
Po drugie, kompilator zastosuje szereg trywialnych konwersji do argumentów w wywołaniu funkcji. Trywialne konwersje to zestaw konkretnych reguł konwersji, które modyfikują typy (bez modyfikowania wartości) w celu znalezienia dopasowania. Należą do nich:
- konwersje lwartości na rwartość
- konwersje kwalifikacyjne (np. non-const na const)
- konwersje bez odniesienia do referencji
Na przykład:
void foo(const int)
{
}
void foo(const double&) // double& is a reference to a double
{
}
int main()
{
int x { 1 };
foo(x); // x trivially converted from int to const int
double d { 2.3 };
foo(d); // d trivially converted from double to const double& (non-ref to ref conversion)
return 0;
}W powyższym przykładzie wywołaliśmy foo(x), gdzie x jest int. Kompilator w prosty sposób przekonwertuje x z int do const int, który następnie dopasuje foo(const int). Zadzwoniliśmy także do foo(d), gdzie d jest double. Kompilator w prosty sposób przekonwertuje d z a double do const double&, który następnie dopasuje foo(const double&).
Powiązana treść
Odniesienia omówimy w lekcji 12.3 -- Odniesienia do wartości.
Dopasowania dokonane poprzez trywialne konwersje są uważane za dokładne dopasowania. Oznacza to, że następujący program daje niejednoznaczne dopasowanie:
void foo(int)
{
}
void foo(const int&) // int& is a reference to a int
{
}
int main()
{
int x { 1 };
foo(x); // ambiguous match with foo(int) and foo(const int&)
return 0;
}Krok 2) Jeśli nie zostanie znalezione dokładne dopasowanie, kompilator próbuje znaleźć dopasowanie, stosując promocję numeryczną do argumentu(ów). Na lekcji (10.1 -- Niejawna konwersja typów) omówiliśmy, w jaki sposób niektóre wąskie typy całkowe i zmiennoprzecinkowe mogą być automatycznie awansowane do szerszych typów, takich jak int lub double. Jeśli po promocji liczbowej zostanie znalezione dopasowanie, wywołanie funkcji zostanie rozwiązane.
Na przykład:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo('a'); // promoted to match foo(int)
foo(true); // promoted to match foo(int)
foo(4.5f); // promoted to match foo(double)
return 0;
}For foo('a'), ponieważ w poprzednim kroku nie można było znaleźć dokładnego dopasowania dla foo(char) , kompilator promuje znak char 'a' na int i szuka dopasowania. To pasuje do foo(int), więc wywołanie funkcji kończy się foo(int).
Krok 3) Jeśli nie zostanie znalezione dopasowanie poprzez promocję numeryczną, kompilator próbuje znaleźć dopasowanie, stosując konwersję numeryczną (10.3 -- Konwersje liczbowe) do argumentów.
Na przykład:
#include <string> // for std::string
void foo(double)
{
}
void foo(std::string)
{
}
int main()
{
foo('a'); // 'a' converted to match foo(double)
return 0;
}W tym przypadku, ponieważ nie ma foo(char) (dokładnego dopasowania) i no foo(int) (dopasowanie promocji), 'a' jest numerycznie konwertowane na wartość podwójną i dopasowywane z foo(double).
Kluczowa informacja
Dopasowania dokonane poprzez zastosowanie promocji numerycznych mają pierwszeństwo przed wszystkimi dopasowaniami uzyskanymi poprzez zastosowanie konwersji numerycznych.
Krok 4) Jeśli w wyniku konwersji liczbowej nie zostanie znalezione dopasowanie, kompilator spróbuje znaleźć dopasowanie poprzez konwersje zdefiniowane przez użytkownika. Chociaż nie omówiliśmy jeszcze konwersji zdefiniowanych przez użytkownika, niektóre typy (np. klasy) mogą definiować konwersje na inne typy, które można wywołać niejawnie. Oto przykład, żeby zilustrować o co chodzi:
// We haven't covered classes yet, so don't worry if this doesn't make sense
class X // this defines a new type called X
{
public:
operator int() { return 0; } // Here's a user-defined conversion from X to int
};
void foo(int)
{
}
void foo(double)
{
}
int main()
{
X x; // Here, we're creating an object of type X (named x)
foo(x); // x is converted to type int using the user-defined conversion from X to int
return 0;
}W tym przykładzie kompilator najpierw sprawdzi, czy istnieje dokładne dopasowanie do foo(X) . Nie zdefiniowaliśmy żadnego. Następnie kompilator sprawdzi, czy x można promować numerycznie, czego nie można. Kompilator sprawdzi następnie, czy x można przekonwertować numerycznie, czego również nie można. Na koniec kompilator będzie szukać wszelkich konwersji zdefiniowanych przez użytkownika. Ponieważ zdefiniowaliśmy konwersję zdefiniowaną przez użytkownika z X Do int, kompilator dokona konwersji X na int w celu dopasowania foo(int).
Po zastosowaniu konwersji zdefiniowanej przez użytkownika kompilator może zastosować dodatkowe ukryte promocje lub konwersje, aby znaleźć dopasowanie. Zatem gdyby nasza konwersja zdefiniowana przez użytkownika polegała na wpisaniu char zamiast int, kompilator użyłby konwersji zdefiniowanej przez użytkownika na char , a następnie promował wynik do int w celu dopasowania.
Powiązana treść
Omawiamy, jak tworzyć konwersje zdefiniowane przez użytkownika dla typów klas (poprzez przeciążanie operatorów rzutowania typów) w lekcji 21.11 -- Przeciążanie typecasts.
Dla zaawansowanych czytelników
Konstruktor klasy działa również jako zdefiniowana przez użytkownika konwersja z innych typów na ten typ klasy i może zostać wykorzystana w tym kroku do znalezienia pasujących funkcji.
Krok 5) Jeśli w wyniku konwersji zdefiniowanej przez użytkownika nie zostanie znalezione żadne dopasowanie, kompilator będzie szukać pasującej funkcji używającej wielokropka.
Powiązana treść
Elipsę omówimy w lekcji 20.5 -- Elipsa (i dlaczego jej unikać).
Krok 6) Jeśli do tego momentu nie znaleziono żadnych dopasowań, kompilator poddaje się i zgłosi błąd kompilacji w związku z niemożnością znalezienia pasującej funkcji.
Niejednoznaczne dopasowania
W przypadku funkcji nieprzeciążonych każde wywołanie funkcji albo zostanie przekształcone w funkcję, albo nie zostanie znalezione żadne dopasowanie i kompilator zgłosi błąd kompilacji:
void foo()
{
}
int main()
{
foo(); // okay: match found
goo(); // compile error: no match found
return 0;
}W przypadku przeciążonych funkcji istnieje trzeci możliwy wynik: można znaleźć ambiguous match . An dopasowaniem niejednoznacznym występuje, gdy kompilator znajdzie dwie lub więcej funkcji, które można dopasować w tym samym kroku. Kiedy to nastąpi, kompilator przestanie dopasowywać i zgłosi błąd kompilacji informujący, że znalazł niejednoznaczne wywołanie funkcji.
Ponieważ każda przeciążona funkcja musi zostać rozróżniona w celu kompilacji, możesz się zastanawiać, jak to możliwe, że wywołanie funkcji może skutkować więcej niż jednym dopasowaniem. Przyjrzyjmy się przykładowi, który to ilustruje:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo(5L); // 5L is type long
return 0;
}Ponieważ literał 5L jest typu long, kompilator najpierw sprawdzi, czy może znaleźć dokładne dopasowanie dla foo(long), ale go nie znajdzie. Następnie kompilator spróbuje promować numerycznie, ale wartości typu long nie mogą być promowane, więc tutaj również nie ma dopasowania.
Następnie kompilator spróbuje znaleźć dopasowanie, stosując konwersje numeryczne do argumentu long . W procesie sprawdzania wszystkich reguł konwersji liczbowej kompilator znajdzie dwa potencjalne dopasowania. Jeśli argument long zostanie przekonwertowany numerycznie na int, wówczas wywołanie funkcji będzie pasować foo(int). Jeśli zamiast tego argument long zostanie przekonwertowany na double, zamiast tego będzie pasował foo(double) . Ponieważ znaleziono dwa możliwe dopasowania poprzez konwersję numeryczną, wywołanie funkcji jest uważane za niejednoznaczne.
W programie Visual Studio 2019 powoduje to następujący komunikat o błędzie:
error C2668: 'foo': ambiguous call to overloaded function message : could be 'void foo(double)' message : or 'void foo(int)' message : while trying to match the argument list '(long)'
Kluczowa informacja
Jeśli kompilator znajdzie wiele dopasowań w danym kroku, spowoduje to niejednoznaczne wywołanie funkcji. Oznacza to, że żadne dopasowanie z danego kroku nie jest uważane za lepsze niż jakiekolwiek inne dopasowanie z tego samego kroku.
Oto kolejny przykład, który daje niejednoznaczne dopasowania:
void foo(unsigned int)
{
}
void foo(float)
{
}
int main()
{
foo(0); // int can be numerically converted to unsigned int or to float
foo(3.14159); // double can be numerically converted to unsigned int or to float
return 0;
}Chociaż można się spodziewać 0 rozwiązać foo(unsigned int) i 3.14159 rozwiązać foo(float), oba te wywołania dają niejednoznaczne dopasowanie. int wartości 0 można numerycznie przekonwertować na unsigned int lub a float, więc każde przeciążenie pasuje równie dobrze, a wynikiem jest niejednoznaczne wywołanie funkcji.
To samo dotyczy konwersji a double na albo a float lub unsigned int. Obie są konwersjami numerycznymi, więc każde przeciążenie pasuje równie dobrze, a wynik znów jest niejednoznaczny.
Dla zaawansowanych czytelników
Domyślne argumenty mogą również powodować niejednoznaczne dopasowania. Omówimy takie przypadki w lekcji 11.5 -- Domyślne argumenty.
Rozwiązywanie niejednoznacznych dopasowań
Ponieważ dopasowania niejednoznaczne są błędem w czasie kompilacji, niejednoznaczne dopasowanie musi zostać ujednoznacznione przed skompilowaniem programu. Istnieje kilka sposobów rozwiązywania niejednoznacznych dopasowań:
- Często najlepszym sposobem jest po prostu zdefiniowanie nowej przeciążonej funkcji, która przyjmuje parametry dokładnie tego typu, z którym próbujesz wywołać tę funkcję. Wtedy C++ będzie mógł znaleźć dokładne dopasowanie dla wywołania funkcji.
- Alternatywnie, jawnie rzuć niejednoznaczne argumenty, aby pasowały do typu funkcji, którą chcesz wywołać. Na przykład, aby
foo(0)dopasowaćfoo(unsigned int)w powyższym przykładzie, możesz zrobić tak:
int x{ 0 };
foo(static_cast<unsigned int>(x)); // will call foo(unsigned int)- Jeśli argument jest literałem, możesz użyć sufiksu literału, aby upewnić się, że literał zostanie zinterpretowany jako poprawny typ:
foo(0u); // will call foo(unsigned int) since 'u' suffix is unsigned int, so this is now an exact matchLista najczęściej używanych przyrostków znajduje się w lekcji 5.2 -- Literały.
Dopasowywanie dla funkcji z wieloma argumenty
Jeśli istnieje wiele argumentów, kompilator stosuje reguły dopasowywania do każdego argumentu po kolei. Wybrana funkcja to ta, dla której każdy argument pasuje co najmniej tak dobrze, jak wszystkie inne funkcje, przy czym co najmniej jeden argument pasuje lepiej niż wszystkie inne funkcje. Innymi słowy, wybrana funkcja musi zapewniać lepsze dopasowanie niż wszystkie inne funkcje kandydujące w przypadku co najmniej jednego parametru i nie gorsze w przypadku wszystkich pozostałych parametrów.
W przypadku znalezienia takiej funkcji jest to zdecydowanie i jednoznacznie najlepszy wybór. Jeżeli nie zostanie znaleziona taka funkcja, wywołanie zostanie uznane za niejednoznaczne (lub niezgodne).
Na przykład:
#include <iostream>
void print(char, int)
{
std::cout << 'a' << '\n';
}
void print(char, double)
{
std::cout << 'b' << '\n';
}
void print(char, float)
{
std::cout << 'c' << '\n';
}
int main()
{
print('x', 'a');
return 0;
}W powyższym programie wszystkie funkcje odpowiadają dokładnie pierwszemu argumentowi. Jednak górna funkcja dopasowuje drugi parametr poprzez promocję, podczas gdy pozostałe funkcje wymagają konwersji. Dlatego print(char, int) jest jednoznacznie najlepszym dopasowaniem.

