Jak działa poniższy kod?
typedef char (&yes)[1];
typedef char (&no)[2];
template <typename B, typename D>
struct Host
{
operator B*() const;
operator D*();
};
template <typename B, typename D>
struct is_base_of
{
template <typename T>
static yes check(D*, T);
static no check(B*, int);
static const bool value = sizeof(check(Host<B,D>(), int())) == sizeof(yes);
};
//Test sample
class Base {};
class Derived : private Base {};
//Expression is true.
int test[is_base_of<Base,Derived>::value && !is_base_of<Derived,Base>::value];
Zauważ, że
B
jest to prywatna baza. Jak to działa?Zauważ, że
operator B*()
jest to const. Dlaczego to jest ważne?Dlaczego jest
template<typename T> static yes check(D*, T);
lepszy niżstatic yes check(B*, int);
?
Uwaga : jest to zredukowana wersja (usunięto makra) programu boost::is_base_of
. Działa to na wielu kompilatorach.
c++
templates
overloading
implicit-conversion
typetraits
Alexey Malistov
źródło
źródło
is_base_of
: ideone.com/T0C1V Nie działa jednak ze starszymi wersjami GCC (GCC4.3 działa dobrze).is_base_of<Base,Base>::value
powinno byćtrue
; to powracafalse
.Odpowiedzi:
Jeśli są spokrewnieni
Załóżmy na chwilę, że
B
tak naprawdę jest to bazaD
. Następnie dla wywołania docheck
obie wersje są opłacalne, ponieważHost
można je przekonwertować naD*
iB*
. Jest to sekwencja konwersji zdefiniowana przez użytkownika, opisana odpowiednio przez13.3.3.1.2
odHost<B, D>
doD*
iB*
. Aby znaleźć funkcje konwersji, które mogą przekształcić klasę, następujące funkcje kandydatów są syntetyzowane dla pierwszejcheck
funkcji zgodnie z13.3.1.5/1
Pierwsza funkcja konwersji nie jest kandydatem, ponieważ
B*
nie można jej przekonwertować naD*
.W przypadku drugiej funkcji istnieją następujący kandydaci:
To są dwie kandydujące funkcje konwersji, które przyjmują obiekt hosta. Pierwsza przyjmuje to przez stałe odniesienie, a druga nie. Zatem druga jest lepiej dopasowana do obiektu innego niż stała
*this
( domniemany argument obiektu )13.3.3.2/3b1sb4
i jest używana do konwersjiB*
na drugącheck
funkcję.Gdybyś usunął stałą, mielibyśmy następujących kandydatów
Oznaczałoby to, że nie możemy już wybierać przez stałość. W zwykłym scenariuszu rozpoznawania przeciążenia wywołanie byłoby teraz niejednoznaczne, ponieważ zwykle typ zwracany nie będzie uczestniczył w rozpoznawaniu przeciążenia. Jednak w przypadku funkcji konwersji istnieje tylne wejście. Jeśli dwie funkcje konwersji są równie dobre, to ich typ zwracany decyduje, która jest najlepsza według
13.3.3/1
. Tak więc, jeśli chcesz usunąć stałą, to zostanie wzięta pierwsza, ponieważB*
konwertuje lepiej naB*
niżD*
naB*
.Jaka sekwencja konwersji zdefiniowana przez użytkownika jest lepsza? Ten na drugą czy pierwszą funkcję kontrolną? Zasada jest taka, że sekwencje konwersji zdefiniowane przez użytkownika mogą być porównywane tylko wtedy, gdy używają tej samej funkcji konwersji lub konstruktora zgodnie z
13.3.3.2/3b2
. Dokładnie tak jest w tym przypadku: Obie używają drugiej funkcji konwersji. Zauważ, że w ten sposób const jest ważny, ponieważ zmusza kompilator do podjęcia drugą funkcję konwersji.Skoro możemy je porównać - który z nich jest lepszy? Zasada jest taka, że lepsza konwersja ze zwracanego typu funkcji konwersji na typ docelowy wygrywa (ponownie przez
13.3.3.2/3b2
). W tym przypadkuD*
konwertuje lepiej naD*
niż naB*
. W ten sposób zostaje wybrana pierwsza funkcja i rozpoznajemy dziedziczenie!Zwróć uwagę, że ponieważ nigdy nie musieliśmy faktycznie konwertować na klasę bazową, możemy w ten sposób rozpoznać dziedziczenie prywatne, ponieważ to, czy możemy przekonwertować z a
D*
na a,B*
nie zależy od formy dziedziczenia zgodnie z4.10/3
Jeśli nie są spokrewnieni
Teraz załóżmy, że nie są one powiązane dziedziczeniem. Zatem dla pierwszej funkcji mamy następujących kandydatów
Po drugie mamy teraz kolejny zestaw
Ponieważ nie możemy dokonać konwersji
D*
na,B*
jeśli nie mamy relacji dziedziczenia, nie mamy teraz wspólnej funkcji konwersji wśród dwóch sekwencji konwersji zdefiniowanych przez użytkownika! Stąd bylibyśmy niejednoznaczni, gdyby nie fakt, że pierwsza funkcja jest szablonem. Szablony są drugim wyborem, gdy istnieje funkcja niebędąca szablonem, która jest równie dobra zgodnie z13.3.3/1
. W ten sposób wybieramy funkcję niebędącą szablonem (drugą) i uznajemy, że nie ma dziedziczenia międzyB
iD
!źródło
std::is_base_of<...>
. Wszystko pod maską.boost::
muszą się upewnić, że mają te funkcje wewnętrzne przed ich użyciem. I mam wrażenie, że jest wśród nich jakaś mentalność „podjętego wyzwania”, aby zaimplementować coś bez pomocy kompilatora :)Sprawdźmy, jak to działa, patrząc na kroki.
Zacznij od
sizeof(check(Host<B,D>(), int()))
części. Kompilator może szybko zobaczyć, żecheck(...)
jest to wyrażenie wywołania funkcji, więc musi wykonać rozpoznanie przeciążeniacheck
. Dostępne są dwa potencjalne przeciążeniatemplate <typename T> yes check(D*, T);
ino check(B*, int);
. Jeśli wybierzesz pierwszy, otrzymaszsizeof(yes)
, w przeciwnym raziesizeof(no)
Następnie przyjrzyjmy się rozdzielczości przeciążenia. Pierwszym przeciążeniem jest wystąpienie szablonu,
check<int> (D*, T=int)
a drugim kandydatem jestcheck(B*, int)
. Rzeczywiste podane argumenty toHost<B,D>
iint()
. Drugi parametr wyraźnie ich nie rozróżnia; posłużyło tylko do tego, aby pierwsze przeciążenie stało się szablonem. Później zobaczymy, dlaczego część szablonu jest odpowiednia.Teraz spójrz na sekwencje konwersji, które są potrzebne. Dla pierwszego przeciążenia mamy
Host<B,D>::operator D*
- jedną konwersję zdefiniowaną przez użytkownika. Po drugie, przeciążenie jest trudniejsze. Potrzebujemy B *, ale prawdopodobnie są dwie sekwencje konwersji. Jeden jest przezHost<B,D>::operator B*() const
. Jeśli (i tylko jeśli) B i D są powiązane przez dziedziczenie, to sekwencja konwersjiHost<B,D>::operator D*()
+ będzieD*->B*
istnieć. Teraz załóżmy, że D rzeczywiście dziedziczy po B. Dwie sekwencje konwersji toHost<B,D> -> Host<B,D> const -> operator B* const -> B*
iHost<B,D> -> operator D* -> D* -> B*
.Tak więc dla pokrewnych B i D
no check(<Host<B,D>(), int())
byłoby niejednoznaczne. W rezultacieyes check<int>(D*, int)
wybierany jest szablon . Jeśli jednak D nie dziedziczy po B,no check(<Host<B,D>(), int())
to nie jest niejednoznaczne. W tym momencie nie można rozwiązać przeciążenia na podstawie najkrótszej sekwencji konwersji. Jednak biorąc pod uwagę równe sekwencje konwersji, rozdzielczość przeciążenia preferuje funkcje inne niż szablonowe, tjno check(B*, int)
.Teraz widzisz, dlaczego nie ma znaczenia, że dziedziczenie jest prywatne: ta relacja służy jedynie do wyeliminowania
no check(Host<B,D>(), int())
z rozwiązywania przeciążenia przed sprawdzeniem dostępu. Widzisz również, dlaczegooperator B* const
musi być const: w przeciwnym razie nie ma potrzeby wykonywaniaHost<B,D> -> Host<B,D> const
kroku, nie ma dwuznaczności ino check(B*, int)
zawsze zostanie wybrany.źródło
const
. Jeśli twoja odpowiedź jest prawdziwa, nieconst
jest potrzebne. Ale to nieprawda. Usuńconst
i sztuczka nie zadziała.no check(B*, int)
nie są już niejednoznaczne.no check(B*, int)
, to dla pokrewnychB
iD
, nie byłoby to dwuznaczne. Kompilator jednoznacznie wybrałbyoperator D*()
wykonanie konwersji, ponieważ nie ma stałej. Jest to raczej trochę w przeciwnym kierunku: jeśli usuniesz stałą, wprowadzisz pewne poczucie niejednoznaczności, ale jest to rozwiązane przez fakt, żeoperator B*()
zapewnia lepszy typ zwrotu, który nie wymaga konwersji wskaźnika, abyB*
lubićD*
.B*
z<Host<B,D>()
tymczasowości.Ten
private
bit jest całkowicie ignorowany przez,is_base_of
ponieważ rozwiązanie przeciążenia występuje przed sprawdzeniami dostępności.Możesz to sprawdzić po prostu:
To samo dotyczy tutaj, fakt, że
B
jest to baza prywatna nie przeszkadza w przeprowadzeniu kontroli, a jedynie uniemożliwi konwersję, ale o samą konwersję nigdy nie prosimy;)źródło
host
jest arbitralnie konwertowany naD*
lubB*
w nieocenionym wyrażeniu. Z jakiegoś powoduD*
jest lepszeB*
pod pewnymi warunkami.Prawdopodobnie ma to coś wspólnego z częściowym porządkowaniem rozwiązania przeciążenia wrt. D * jest bardziej wyspecjalizowany niż B * w przypadku, gdy D pochodzi od B.
Dokładne szczegóły są dość skomplikowane. Musisz znaleźć pierwszeństwo różnych reguł rozwiązywania przeciążeń. Częściowe zamówienie to jedno. Kolejne jest długości / rodzaje sekwencji konwersji. Wreszcie, jeśli dwie realne funkcje zostaną uznane za równie dobre, zamiast szablonów funkcji wybierane są inne niż szablony.
Nigdy nie musiałem sprawdzać, jak te zasady współdziałają. Ale wydaje się, że częściowe porządkowanie dominuje w innych regułach rozwiązywania przeciążeń. Gdy D nie pochodzi od B, reguły częściowego porządkowania nie mają zastosowania, a nie-szablon jest bardziej atrakcyjny. Gdy D pochodzi od B, porządkowanie częściowe zaczyna działać i sprawia, że szablon funkcji jest bardziej atrakcyjny - jak się wydaje.
Jeśli chodzi o dziedziczenie prywatne: kod nigdy nie prosi o konwersję z D * na B *, co wymagałoby dziedziczenia publicznego.
źródło
is_base_of
i pętli, przez które przeszli współtwórcy, aby to zapewnić.The exact details are rather complicated
- o to chodzi. Proszę wytłumacz. Chcę wiedzieć.Kontynuując drugie pytanie, zwróć uwagę, że gdyby nie const, Host byłby źle sformułowany, gdyby został utworzony za pomocą B == D.Ale is_base_of jest zaprojektowany w taki sposób, że każda klasa jest bazą samą w sobie, dlatego jeden z operatorów konwersji musi być konst.
źródło