Jak działa „is_base_of”?

118

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];
  1. Zauważ, że Bjest to prywatna baza. Jak to działa?

  2. Zauważ, że operator B*()jest to const. Dlaczego to jest ważne?

  3. 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.

Alexey Malistov
źródło
4
Używanie tego samego identyfikatora dla parametru szablonu i prawdziwej nazwy klasy jest bardzo mylące ...
Matthieu M.
1
@Matthieu M., wziąłem się na siebie, aby poprawić :)
Kirill V. Lyadvinsky
2
Jakiś czas temu napisałem alternatywną implementację is_base_of: ideone.com/T0C1V Nie działa jednak ze starszymi wersjami GCC (GCC4.3 działa dobrze).
Johannes Schaub - litb
3
Ok, idę na spacer.
jokoon
2
Ta implementacja jest nieprawidłowa. is_base_of<Base,Base>::valuepowinno być true; to powraca false.
chengiz

Odpowiedzi:

109

Jeśli są spokrewnieni

Załóżmy na chwilę, że Btak naprawdę jest to baza D. Następnie dla wywołania do checkobie wersje są opłacalne, ponieważ Hostmożna je przekonwertować na D* i B* . Jest to sekwencja konwersji zdefiniowana przez użytkownika, opisana odpowiednio przez 13.3.3.1.2od Host<B, D>do D*i B*. Aby znaleźć funkcje konwersji, które mogą przekształcić klasę, następujące funkcje kandydatów są syntetyzowane dla pierwszej checkfunkcji zgodnie z13.3.1.5/1

D* (Host<B, D>&)

Pierwsza funkcja konwersji nie jest kandydatem, ponieważ B*nie można jej przekonwertować na D*.

W przypadku drugiej funkcji istnieją następujący kandydaci:

B* (Host<B, D> const&)
D* (Host<B, D>&)

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/3b1sb4i jest używana do konwersji B*na drugą checkfunkcję.

Gdybyś usunął stałą, mielibyśmy następujących kandydatów

B* (Host<B, D>&)
D* (Host<B, D>&)

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 na B*niż D*na B*.

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 przypadku D*konwertuje lepiej na D*niż na B*. 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

D* (Host<B, D>&) 

Po drugie mamy teraz kolejny zestaw

B* (Host<B, D> const&)

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 z 13.3.3/1. W ten sposób wybieramy funkcję niebędącą szablonem (drugą) i uznajemy, że nie ma dziedziczenia między Bi D!

Johannes Schaub - litb
źródło
2
Ach! Andreas miał poprawny akapit, szkoda, że ​​nie udzielił takiej odpowiedzi :) Dziękuję za poświęcony czas, chciałbym umieścić ulubione.
Matthieu M.
2
To będzie moja ulubiona odpowiedź kiedykolwiek ... pytanie: czy przeczytałeś cały standard C ++, czy po prostu pracujesz w komisji C ++? Gratulacje!
Marco A.
4
@DavidKernin pracujący w komitecie C ++ nie sprawia, że ​​automatycznie wiesz, jak działa C ++ :) Więc zdecydowanie musisz przeczytać część Standardu, która jest potrzebna, aby poznać szczegóły, które zrobiłem. Nie przeczytałem tego wszystkiego, więc zdecydowanie nie mogę pomóc w większości pytań dotyczących biblioteki Standard lub wątków :)
Johannes Schaub - litb
1
@underscore_d Aby być uczciwym, specyfikacja nie zabrania std :: traits używania jakiejś magii kompilatora, aby standardowe implementatory bibliotek mogły używać ich według własnego uznania . Unikną szablonów akrobatycznych, co również pomaga przyspieszyć czas kompilacji i zużycie pamięci. Dzieje się tak, nawet jeśli interfejs wygląda jak std::is_base_of<...>. Wszystko pod maską.
Johannes Schaub - litb
2
Oczywiście biblioteki ogólne 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 :)
Johannes Schaub - litb
24

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ć, że check(...)jest to wyrażenie wywołania funkcji, więc musi wykonać rozpoznanie przeciążenia check. Dostępne są dwa potencjalne przeciążenia template <typename T> yes check(D*, T);i no check(B*, int);. Jeśli wybierzesz pierwszy, otrzymasz sizeof(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 jest check(B*, int). Rzeczywiste podane argumenty to Host<B,D>i int(). 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 przez Host<B,D>::operator B*() const. Jeśli (i tylko jeśli) B i D są powiązane przez dziedziczenie, to sekwencja konwersji Host<B,D>::operator D*()+ będzie D*->B*istnieć. Teraz załóżmy, że D rzeczywiście dziedziczy po B. Dwie sekwencje konwersji to Host<B,D> -> Host<B,D> const -> operator B* const -> B*i Host<B,D> -> operator D* -> D* -> B*.

Tak więc dla pokrewnych B i D no check(<Host<B,D>(), int())byłoby niejednoznaczne. W rezultacie yes 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, tj no 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ż, dlaczego operator B* constmusi być const: w przeciwnym razie nie ma potrzeby wykonywania Host<B,D> -> Host<B,D> constkroku, nie ma dwuznaczności i no check(B*, int)zawsze zostanie wybrany.

MSalters
źródło
Twoje wyjaśnienie nie wyjaśnia obecności const. Jeśli twoja odpowiedź jest prawdziwa, nie constjest potrzebne. Ale to nieprawda. Usuń consti sztuczka nie zadziała.
Alexey Malistov
Bez stałej dwie sekwencje konwersji dla no check(B*, int)nie są już niejednoznaczne.
MSalters
Jeśli wyjdziesz tylko no check(B*, int), to dla pokrewnych Bi D, nie byłoby to dwuznaczne. Kompilator jednoznacznie wybrałby operator 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, że operator B*()zapewnia lepszy typ zwrotu, który nie wymaga konwersji wskaźnika, aby B*lubić D*.
Johannes Schaub - litb
W istocie o to chodzi: niejednoznaczność występuje między dwiema różnymi sekwencjami konwersji, aby uzyskać a B*z <Host<B,D>()tymczasowości.
MSalters
To jest lepsza odpowiedź. Dzięki! Tak więc, jak zrozumiałem, jeśli jedna funkcja jest lepsza, ale niejednoznaczna, to wybrana zostanie inna funkcja?
user1289
4

Ten privatebit jest całkowicie ignorowany przez, is_base_ofponieważ rozwiązanie przeciążenia występuje przed sprawdzeniami dostępności.

Możesz to sprawdzić po prostu:

class Foo
{
public:
  void bar(int);
private:
  void bar(double);
};

int main(int argc, char* argv[])
{
  Foo foo;
  double d = 0.3;
  foo.bar(d);       // Compiler error, cannot access private member function
}

To samo dotyczy tutaj, fakt, że Bjest to baza prywatna nie przeszkadza w przeprowadzeniu kontroli, a jedynie uniemożliwi konwersję, ale o samą konwersję nigdy nie prosimy;)

Matthieu M.
źródło
Raczej. W ogóle nie jest wykonywana żadna konwersja podstaw. hostjest arbitralnie konwertowany na D*lub B*w nieocenionym wyrażeniu. Z jakiegoś powodu D*jest lepsze B*pod pewnymi warunkami.
Potatoswatter
Myślę, że odpowiedź znajduje się w 13.3.1.1.2, ale jeszcze nie uporządkowałem szczegółów :)
Andreas Brinck
Moja odpowiedź wyjaśnia tylko część „dlaczego nawet prace prywatne”, odpowiedź sellibitze jest z pewnością bardziej kompletna, chociaż z niecierpliwością czekam na jasne wyjaśnienie pełnego procesu rozwiązywania w zależności od przypadków.
Matthieu M.
2

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.

sellibitze
źródło
Myślę, że to coś takiego, pamiętam, że widziałem obszerną dyskusję na temat archiwów boost na temat implementacji is_base_ofi pętli, przez które przeszli współtwórcy, aby to zapewnić.
Matthieu M.
The exact details are rather complicated- o to chodzi. Proszę wytłumacz. Chcę wiedzieć.
Alexey Malistov
@Alexey: Cóż, myślałem, że wskazałem ci właściwy kierunek. Sprawdź, jak w tym przypadku współdziałają różne reguły rozpoznawania przeciążenia. Jedyną różnicą między D pochodzącym z B i D niepochodzącym z B w odniesieniu do rozwiązania tego przypadku przeciążenia jest reguła częściowego uporządkowania. Rozwiązanie problemu z przeciążeniem opisano w §13 standardu C ++. Możesz otrzymać wersję roboczą za darmo: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
Rozwiązanie problemu przeciążenia obejmuje 16 stron w tej wersji roboczej. Myślę, że jeśli naprawdę potrzebujesz zrozumieć zasady i interakcje między nimi w tym przypadku, powinieneś przeczytać całą sekcję §13.3. Nie liczyłbym na odpowiedź, która jest w 100% poprawna i spełniająca Twoje standardy.
sellibitze
proszę zobaczyć moją odpowiedź, aby wyjaśnić to, jeśli jesteś zainteresowany.
Johannes Schaub - litb
0

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.

Herc
źródło