C++
Overbelasting resolutie
Zoeken…
Opmerkingen
Overbelastingsresolutie gebeurt in verschillende situaties
- Roept naar overbelaste functies met de naam. De kandidaten zijn alle functies gevonden op naam opzoeken.
- Oproepen naar klasseobject. De kandidaten zijn meestal alle overbelaste functie-oproepoperators van de klas.
- Gebruik van een operator. De kandidaten zijn de overbelaste operatorfuncties op naamruimtebereik, de overbelaste operatorfuncties in het linker klasseobject (indien aanwezig) en de ingebouwde operators.
- Overbelastingsresolutie om de juiste conversieoperatorfunctie of constructor te vinden voor een initialisatie
- Voor niet-lijst directe initialisatie (
Class c(value)
) zijn de kandidaten constructeurs vanClass
. - Voor niet-lijst kopieerinitialisatie (
Class c = value
) en voor het vinden van de door de gebruiker gedefinieerde conversiefunctie om in een door de gebruiker gedefinieerde conversie te activeren. De kandidaten zijn de constructors vanClass
en als de bron een class-object is, functioneert de conversie-operator. - Voor initialisatie van een niet-klasse uit een
Nonclass c = classObject
(Nonclass c = classObject
). De kandidaten zijn de conversieoperatorfuncties van het initialisatieobject. - Voor het initialiseren van een referentie met een
R &r = classObject
(R &r = classObject
), wanneer de klasse conversieoperatorfuncties heeft die waarden opleveren die direct aanr
kunnen worden gebonden. De kandidaten zijn dergelijke conversie-operatorfuncties. - Voor lijstinitialisatie van een niet-geaggregeerd klasseobject (
Class c{1, 2, 3}
) zijn de kandidaten de constructeurs van de initialisatielijst voor een first-pass via overbelastingsresolutie. Als dit geen haalbare kandidaat vindt, wordt er een tweede doorgang gedaan via overbelastingsresolutie, met de constructeurs vanClass
als kandidaten.
- Voor niet-lijst directe initialisatie (
Exacte overeenkomst
Een overbelasting zonder conversies die nodig is voor parametertypen of alleen conversies die nodig zijn tussen typen die nog steeds als exacte overeenkomsten worden beschouwd, heeft de voorkeur boven een overbelasting die andere conversies vereist om te kunnen bellen.
void f(int x);
void f(double x);
f(42); // calls f(int)
Wanneer een argument bindt aan een verwijzing naar hetzelfde type, wordt de overeenkomst geacht geen conversie te vereisen, zelfs als de verwijzing meer cv-gekwalificeerd is.
void f(int& x);
void f(double x);
int x = 42;
f(x); // argument type is int; exact match with int&
void g(const int& x);
void g(int x);
g(x); // ambiguous; both overloads give exact match
Ten behoeve van de overbelastingsresolutie wordt het type "array van T
" geacht exact overeen te komen met het type "pointer naar T
", en wordt het functietype T
geacht exact overeen te komen met het functiepointer type T*
, hoewel beide vereisen conversies.
void f(int* p);
void f(void* p);
void g(int* p);
void g(int (&p)[100]);
int a[100];
f(a); // calls f(int*); exact match with array-to-pointer conversion
g(a); // ambiguous; both overloads give exact match
Indeling van argument naar parameterkosten
Resolutie voor overbelasting verdeelt de kosten van het doorgeven van een argument aan een parameter in een van de vier verschillende categorieën, "sequenties" genoemd. Elke reeks kan nul, één of meerdere conversies bevatten
Standaard conversie volgorde
void f(int a); f(42);
Door de gebruiker gedefinieerde conversie
void f(std::string s); f("hello");
Ellipsis conversie volgorde
void f(...); f(42);
Lijst initialisatie volgorde
void f(std::vector<int> v); f({1, 2, 3});
Het algemene principe is dat standaardconversiesequenties het goedkoopst zijn, gevolgd door door de gebruiker gedefinieerde conversiesequenties, gevolgd door ellipsisconversiesequenties.
Een speciaal geval is de initialisatie van de lijst, die geen conversie vormt (een initialisatielijst is geen uitdrukking met een type). De kosten worden bepaald door te definiëren dat het equivalent is aan een van de andere drie conversie-reeksen, afhankelijk van het parametertype en de vorm van de initialisatielijst.
Naam opzoeken en toegangscontrole
Overbelastingsresolutie treedt op na het opzoeken van de naam. Dit betekent dat een beter passende functie niet wordt geselecteerd door overbelastingsresolutie als de naam niet meer wordt opgezocht:
void f(int x);
struct S {
void f(double x);
void g() { f(42); } // calls S::f because global f is not visible here,
// even though it would be a better match
};
Overbelastingsresolutie treedt op vóór toegangscontrole. Een ontoegankelijke functie kan worden geselecteerd door overbelastingsresolutie als deze beter overeenkomt dan een toegankelijke functie.
class C {
public:
static void f(double x);
private:
static void f(int x);
};
C::f(42); // Error! Calls private C::f(int) even though public C::f(double) is viable.
Evenzo gebeurt overbelastingsresolutie zonder te controleren of de resulterende oproep goed gevormd is met betrekking tot explicit
:
struct X {
explicit X(int );
X(char );
};
void foo(X );
foo({4}); // X(int) is better much, but expression is
// ill-formed because selected constructor is explicit
Overbelasting op doorstuurreferentie
Je moet heel voorzichtig zijn bij het verstrekken van een referentie-overbelasting voor doorsturen, omdat deze te goed kan overeenkomen:
struct A {
A() = default; // #1
A(A const& ) = default; // #2
template <class T>
A(T&& ); // #3
};
De bedoeling was dat A
kan worden gekopieerd en dat we deze andere constructor hebben die een ander lid kan initialiseren. Echter:
A a; // calls #1
A b(a); // calls #3!
Er zijn twee uitvoerbare wedstrijden voor de bouwoproep:
A(A const& ); // #2
A(A& ); // #3, with T = A&
Beide zijn Exacte overeenkomsten, maar #3
verwijst naar een minder cv- gekwalificeerd object dan #2
, dus het heeft de betere standaardconversiesequentie en is de best haalbare functie.
De oplossing hier is om deze constructors altijd te beperken (bijvoorbeeld door SFINAE te gebruiken):
template <class T,
class = std::enable_if_t<!std::is_convertible<std::decay_t<T>*, A*>::value>
>
A(T&& );
Het type kenmerk hier is om elke A
of klasse publiekelijk en ondubbelzinnig afgeleid van A
uit overweging te nemen, waardoor deze constructor slecht gevormd zou worden in het eerder beschreven voorbeeld (en dus verwijderd uit de overbelastingsset). Als gevolg hiervan wordt de kopie-constructor aangeroepen - wat we wilden.
Stappen van overbelastingsresolutie
De stappen van overbelastingsresolutie zijn:
Zoek kandidaatfuncties via naam opzoeken. Niet-gekwalificeerde oproepen voeren zowel reguliere niet-gekwalificeerde opzoekopdrachten als argumentafhankelijke opzoekopdrachten uit (indien van toepassing).
Filter de set kandidaatfuncties naar een set uitvoerbare functies. Een uitvoerbare functie waarvoor een impliciete conversie bestaat tussen de argumenten waarmee de functie wordt aangeroepen en de parameters die de functie aanneemt.
void f(char); // (1) void f(int ) = delete; // (2) void f(); // (3) void f(int& ); // (4) f(4); // 1,2 are viable (even though 2 is deleted!) // 3 is not viable because the argument lists don't match // 4 is not viable because we cannot bind a temporary to // a non-const lvalue reference
Kies de best haalbare kandidaat. Een uitvoerbare functie
F1
is een betere functie dan een andere uitvoerbare functieF2
als de impliciete conversiesequentie voor elk argument inF1
niet slechter is dan de overeenkomstige impliciete conversiesequentie inF2
, en ...:3.1. Voor sommige argumenten is de impliciete conversiesequentie voor dat argument in
F1
een betere conversiesequentie dan voor dat argument inF2
, ofvoid f(int ); // (1) void f(char ); // (2) f(4); // call (1), better conversion sequence
3.2. In een door de gebruiker gedefinieerde conversie is de standaardconversiereeks van de terugkeer van
F1
naar het bestemmingstype een betere conversie-volgorde dan die van het retourtype vanF2
, ofstruct A { operator int(); operator double(); } a; int i = a; // a.operator int() is better than a.operator double() and a conversion float f = a; // ambiguous
3.3. In een directe referentiebinding heeft
F1
dezelfde soort referentie doorF2
niet, ofstruct A { operator X&(); // #1 operator X&&(); // #2 }; A a; X& lx = a; // calls #1 X&& rx = a; // calls #2
3.4.
F1
is geen functiesjabloon-specialisatie, maarF2
is, oftemplate <class T> void f(T ); // #1 void f(int ); // #2 f(42); // calls #2, the non-template
3.5.
F1
enF2
zijn beide functiesjabloon-specialisaties, maarF1
is meer gespecialiseerd danF2
.template <class T> void f(T ); // #1 template <class T> void f(T* ); // #2 int* p; f(p); // calls #2, more specialized
De volgorde hier is aanzienlijk. De betere conversie van de volgorde van conversies vindt plaats voordat de sjabloon versus niet-sjablooncontrole. Dit leidt tot een veel voorkomende fout bij overbelasting bij het doorsturen van referentie:
struct A {
A(A const& ); // #1
template <class T>
A(T&& ); // #2, not constrained
};
A a;
A b(a); // calls #2!
// #1 is not a template but #2 resolves to
// A(A& ), which is a less cv-qualified reference than #1
// which makes it a better implicit conversion sequence
Als er aan het eind geen enkele best haalbare kandidaat is, is de oproep dubbelzinnig:
void f(double ) { }
void f(float ) { }
f(42); // error: ambiguous
Rekenkundige promoties en conversies
Het is beter om een geheel getal naar het overeenkomstige gepromoveerde type te converteren dan naar een ander geheel getal.
void f(int x);
void f(short x);
signed char c = 42;
f(c); // calls f(int); promotion to int is better than conversion to short
short s = 42;
f(s); // calls f(short); exact match is better than promotion to int
Het is beter om een float
te double
dan deze te converteren naar een ander drijvend punttype.
void f(double x);
void f(long double x);
f(3.14f); // calls f(double); promotion to double is better than conversion to long double
Rekenkundige conversies anders dan promoties zijn niet beter of slechter dan elkaar.
void f(float x);
void f(long double x);
f(3.14); // ambiguous
void g(long x);
void g(long double x);
g(42); // ambiguous
g(3.14); // ambiguous
Om er zeker van te zijn dat er geen dubbelzinnigheid is bij het aanroepen van een functie f
met integrale of floating-point argumenten van elk standaardtype, zijn in totaal acht overbelastingen nodig, zodat voor elk mogelijk argumenttype een overbelasting overeenkomt exact of de unieke overbelasting met het gepromoveerde argumenttype wordt geselecteerd.
void f(int x);
void f(unsigned int x);
void f(long x);
void f(unsigned long x);
void f(long long x);
void f(unsigned long long x);
void f(double x);
void f(long double x);
Overbelasting binnen een klassenhiërarchie
In de volgende voorbeelden wordt deze klassenhiërarchie gebruikt:
struct A { int m; };
struct B : A {};
struct C : B {};
De conversie van afgeleid klasse-type naar basisklasse-type heeft de voorkeur boven door de gebruiker gedefinieerde conversies. Dit is van toepassing bij het doorgeven van waarde of referentie, evenals bij het converteren van aanwijzer naar afgeleid naar aanwijzer naar basis.
struct Unrelated {
Unrelated(B b);
};
void f(A a);
void f(Unrelated u);
B b;
f(b); // calls f(A)
Een pointerconversie van afgeleide klasse naar basisklasse is ook beter dan conversie naar void*
.
void f(A* p);
void f(void* p);
B b;
f(&b); // calls f(A*)
Als er meerdere overbelastingen binnen dezelfde overervingsketen zijn, heeft de meest afgeleide basisklasse overbelasting de voorkeur. Dit is gebaseerd op een soortgelijk principe als virtuele verzending: de "meest gespecialiseerde" implementatie wordt gekozen. Resolutie overbelasting treedt echter altijd op tijdens het compileren en wordt nooit impliciet down-cast.
void f(const A& a);
void f(const B& b);
C c;
f(c); // calls f(const B&)
B b;
A& r = b;
f(r); // calls f(const A&); the f(const B&) overload is not viable
Voor verwijzingen naar leden, die tegenstrijdig zijn met betrekking tot de klasse, geldt een vergelijkbare regel in de tegenovergestelde richting: de minst afgeleide afgeleide klasse heeft de voorkeur.
void f(int B::*p);
void f(int C::*p);
int A::*p = &A::m;
f(p); // calls f(int B::*)
Overbelasting van constness en volatiliteit
Het passeren van een pointer argument voor een T*
parameter, indien mogelijk, is beter dan doorgeeft aan een const T*
parameter.
struct Base {};
struct Derived : Base {};
void f(Base* pb);
void f(const Base* pb);
void f(const Derived* pd);
void f(bool b);
Base b;
f(&b); // f(Base*) is better than f(const Base*)
Derived d;
f(&d); // f(const Derived*) is better than f(Base*) though;
// constness is only a "tie-breaker" rule
Evenzo is een argument doorgeven aan een T&
-parameter, indien mogelijk, beter dan het doorgeven aan een const T&
-parameter, zelfs als beide een exacte matchrang hebben.
void f(int& r);
void f(const int& r);
int x;
f(x); // both overloads match exactly, but f(int&) is still better
const int y = 42;
f(y); // only f(const int&) is viable
Deze regel is ook van toepassing op const-gekwalificeerde lidfuncties, waar het belangrijk is om veranderlijke toegang toe te staan tot niet-const objecten en onveranderlijke toegang tot const objecten.
class IntVector {
public:
// ...
int* data() { return m_data; }
const int* data() const { return m_data; }
private:
// ...
int* m_data;
};
IntVector v1;
int* data1 = v1.data(); // Vector::data() is better than Vector::data() const;
// data1 can be used to modify the vector's data
const IntVector v2;
const int* data2 = v2.data(); // only Vector::data() const is viable;
// data2 can't be used to modify the vector's data
Op dezelfde manier zal een vluchtige overbelasting minder de voorkeur hebben dan een niet-vluchtige overbelasting.
class AtomicInt {
public:
// ...
int load();
int load() volatile;
private:
// ...
};
AtomicInt a1;
a1.load(); // non-volatile overload preferred; no side effect
volatile AtomicInt a2;
a2.load(); // only volatile overload is viable; side effect
static_cast<volatile AtomicInt&>(a1).load(); // force volatile semantics for a1