Come funziona `is_base_of`?


118

Come funziona il codice seguente?

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. Nota che Bè una base privata. Come funziona?

  2. Nota che operator B*()è const. Perché è importante?

  3. Perché è template<typename T> static yes check(D*, T);meglio di static yes check(B*, int);?

Nota : è una versione ridotta (le macro vengono rimosse) di boost::is_base_of. E questo funziona su un'ampia gamma di compilatori.


4
È molto confuso da parte tua usare lo stesso identificatore per un parametro di modello e un vero nome di classe ...
Matthieu M.

1
@ Matthieu M., mi sono preso la responsabilità di correggere :)
Kirill V. Lyadvinsky,

2
Qualche tempo fa ho scritto un'implementazione alternativa di is_base_of: ideone.com/T0C1V Tuttavia non funziona con le versioni precedenti di GCC (GCC4.3 funziona bene).
Johannes Schaub - litb

3
Ok, vado a fare una passeggiata.
jokoon

2
Questa implementazione non è corretta. is_base_of<Base,Base>::valuedovrebbe essere true; questo ritorna false.
chengiz

Risposte:


109

Se sono correlati

Supponiamo per un momento che Bsia effettivamente una base di D. Quindi per la chiamata a check, entrambe le versioni sono valide perché Hostpossono essere convertite in D* e B* . È una sequenza di conversione definita dall'utente come descritta rispettivamente da 13.3.3.1.2da Host<B, D>a D*e B*. Per trovare funzioni di conversione che possono convertire la classe, le seguenti funzioni candidate vengono sintetizzate per la prima checkfunzione secondo13.3.1.5/1

D* (Host<B, D>&)

La prima funzione di conversione non è una candidata, perché B*non può essere convertita in D*.

Per la seconda funzione esistono i seguenti candidati:

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

Questi sono i due candidati alla funzione di conversione che accettano l'oggetto host. Il primo lo prende per riferimento const, e il secondo no. Quindi il secondo è una corrispondenza migliore per l' *thisoggetto non const (l' argomento dell'oggetto implicito ) da 13.3.3.2/3b1sb4e viene utilizzato per la conversione B*per la seconda checkfunzione.

Se rimuovessi const, avremmo i seguenti candidati

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

Ciò significherebbe che non possiamo più selezionare per costanza. In uno scenario di risoluzione dell'overload ordinario, la chiamata ora sarebbe ambigua perché normalmente il tipo restituito non parteciperà alla risoluzione dell'overload. Per le funzioni di conversione, tuttavia, esiste una backdoor. Se due funzioni di conversione sono ugualmente valide, il tipo restituito decide chi è il migliore in base a 13.3.3/1. Pertanto, se si rimuove la const, verrà presa la prima, perché si B*converte meglio in B*che D*in B*.

Ora quale sequenza di conversione definita dall'utente è migliore? Quello per la seconda o la prima funzione di controllo? La regola è che le sequenze di conversione definite dall'utente possono essere confrontate solo se utilizzano la stessa funzione di conversione o costruttore secondo 13.3.3.2/3b2. Questo è esattamente il caso qui: entrambi usano la seconda funzione di conversione. Si noti che quindi const è importante perché forza il compilatore a prendere la seconda funzione di conversione.

Dal momento che possiamo confrontarli, qual è il migliore? La regola è che la migliore conversione dal tipo di ritorno della funzione di conversione al tipo di destinazione vince (di nuovo da 13.3.3.2/3b2). In questo caso, D*converte meglio in D*che in B*. Così viene selezionata la prima funzione e riconosciamo l'eredità!

Si noti che poiché non abbiamo mai avuto bisogno di convertirci effettivamente in una classe base, possiamo quindi riconoscere l'ereditarietà privata perché se possiamo convertire da a D*a a B*non dipende dalla forma di ereditarietà secondo4.10/3

Se non sono correlati

Supponiamo ora che non siano correlati per eredità. Quindi per la prima funzione abbiamo i seguenti candidati

D* (Host<B, D>&) 

E per il secondo ora abbiamo un altro set

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

Poiché non possiamo convertire D*in B*se non abbiamo una relazione di ereditarietà, ora non abbiamo una funzione di conversione comune tra le due sequenze di conversione definite dall'utente! Pertanto, saremmo ambigui se non fosse per il fatto che la prima funzione è un modello. I modelli sono la seconda scelta quando esiste una funzione non modello che è altrettanto buona secondo 13.3.3/1. Quindi, selezioniamo la funzione non modello (seconda) e riconosciamo che non esiste eredità tra Be D!


2
Ah! Andreas aveva il paragrafo giusto, peccato che non abbia dato una risposta del genere :) Grazie per il tuo tempo, vorrei poterlo mettere come preferito.
Matthieu M.

2
Questa sarà la mia risposta preferita in assoluto ... una domanda: hai letto l'intero standard C ++ o stai solo lavorando nel comitato C ++ ?? Congratulazioni!
Marco A.

4
@DavidKernin lavorando nel comitato C ++ non ti fa sapere automaticamente come funziona C ++ :) Quindi devi assolutamente leggere la parte dello Standard che è necessaria per conoscere i dettagli, cosa che ho fatto. Non l'ho letto tutto, quindi non posso assolutamente aiutare con la maggior parte della libreria Standard o delle domande relative al threading :)
Johannes Schaub - litb

1
@underscore_d Per essere onesti, la specifica non proibisce allo std :: traits di usare un po 'di magia del compilatore in modo che gli implementatori di librerie standard possano usarli a loro piacimento . Eviteranno le acrobazie dei modelli che aiutano anche ad accelerare i tempi di compilazione e l'utilizzo della memoria. Questo è vero anche se l' interfaccia sembra std::is_base_of<...>. È tutto sotto il cofano.
Johannes Schaub - litb

2
Ovviamente, le biblioteche generali hanno boost::bisogno di assicurarsi di avere questi elementi intrinseci disponibili prima di utilizzarli. E ho la sensazione che ci sia una sorta di mentalità da "sfida accettata" tra loro per implementare le cose senza l'aiuto del compilatore :)
Johannes Schaub - litb

24

Scopriamo come funziona guardando i passaggi.

Inizia con la sizeof(check(Host<B,D>(), int()))parte. Il compilatore può vedere rapidamente che questa check(...)è un'espressione di chiamata di funzione, quindi deve eseguire la risoluzione dell'overload check. Sono disponibili due overload candidati template <typename T> yes check(D*, T);e no check(B*, int);. Se viene scelto il primo, ottieni sizeof(yes), altrimentisizeof(no)

Successivamente, esaminiamo la risoluzione del sovraccarico. Il primo overload è un'istanza del modello check<int> (D*, T=int)e il secondo candidato lo è check(B*, int). Gli argomenti effettivi forniti sono Host<B,D>e int(). Il secondo parametro chiaramente non li distingue; serviva semplicemente a rendere il primo overload un modello. Vedremo più avanti perché la parte del modello è rilevante.

Ora guarda le sequenze di conversione necessarie. Per il primo sovraccarico, abbiamo Host<B,D>::operator D*: una conversione definita dall'utente. Per il secondo, il sovraccarico è più complicato. Abbiamo bisogno di un B *, ma forse ci sono due sequenze di conversione. Uno è via Host<B,D>::operator B*() const. Se (e solo se) B e D sono legati per eredità, la sequenza di conversione Host<B,D>::operator D*()+ D*->B*esisterà. Supponiamo ora che D erediti effettivamente da B. Le due sequenze di conversione sono Host<B,D> -> Host<B,D> const -> operator B* const -> B*e Host<B,D> -> operator D* -> D* -> B*.

Quindi, per i correlati B e D, no check(<Host<B,D>(), int())sarebbe ambiguo. Di conseguenza, yes check<int>(D*, int)viene scelto il modello . Tuttavia, se D non eredita da B, no check(<Host<B,D>(), int())non è ambiguo. A questo punto, la risoluzione del sovraccarico non può avvenire in base alla sequenza di conversione più breve. Tuttavia, a parità di sequenze di conversione, la risoluzione del sovraccarico preferisce funzioni non template, ad es no check(B*, int).

Ora capisci perché non importa che l'eredità sia privata: quella relazione serve solo ad eliminare no check(Host<B,D>(), int())dalla risoluzione del sovraccarico prima che avvenga il controllo dell'accesso. E vedi anche perché il operator B* constdeve essere const: altrimenti non c'è bisogno del Host<B,D> -> Host<B,D> constpasso, nessuna ambiguità, e no check(B*, int)verrebbe sempre scelto.


La tua spiegazione non tiene conto della presenza di const. Se la tua risposta è vera, allora non constè necessario. Ma non è vero. Rimuovi conste il trucco non funzionerà.
Alexey Malistov

Senza const le due sequenze di conversione per no check(B*, int)non sono più ambigue.
MSalters

Se lasci solo no check(B*, int), quindi per correlato Be D, non sarebbe ambiguo. Il compilatore sceglierebbe in modo inequivocabile operator D*()di eseguire la conversione perché non ha un const. È piuttosto un po 'nella direzione opposta: se rimuovi il const, introduci un certo senso di ambiguità, ma che viene risolto dal fatto che operator B*()fornisce un tipo di ritorno superiore che non necessita di una conversione del puntatore a B*come D*fa.
Johannes Schaub - litb

Questo è davvero il punto: l'ambiguità è tra le due diverse sequenze di conversione per ottenere un B*dal <Host<B,D>()temporaneo.
MSalters

Questa è una risposta migliore. Grazie! Quindi, come ho capito, se una funzione è migliore, ma ambigua, viene scelta un'altra funzione?
user1289

4

Il privatebit viene completamente ignorato da is_base_ofperché la risoluzione del sovraccarico avviene prima dei controlli di accessibilità.

Puoi verificarlo semplicemente:

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
}

Lo stesso vale qui, il fatto che Bsia una base privata non impedisce che il controllo abbia luogo, impedirebbe solo la conversione, ma non chiediamo mai la conversione effettiva;)


Una specie di. Non viene eseguita alcuna conversione di base. hostviene arbitrariamente convertito in D*o B*nell'espressione non valutata. Per qualche ragione, D*è preferibile rispetto a B*determinate condizioni.
Potatoswatter

Penso che la risposta sia in 13.3.1.1.2 ma devo ancora chiarire i dettagli :)
Andreas Brinck

La mia risposta spiega solo la parte "perché anche lavori privati", la risposta di sellibitze è certamente più completa anche se aspetto con impazienza una spiegazione chiara del processo di risoluzione completa a seconda dei casi.
Matthieu M.

2

Forse ha qualcosa a che fare con l'ordinamento parziale rispetto alla risoluzione del sovraccarico. D * è più specializzato di B * nel caso in cui D deriva da B.

I dettagli esatti sono piuttosto complicati. Devi capire le priorità di varie regole di risoluzione del sovraccarico. L'ordinamento parziale è uno. Lunghezze / tipi di sequenze di conversione è un altro. Infine, se due funzioni praticabili sono ritenute ugualmente valide, i modelli non vengono scelti rispetto ai modelli di funzione.

Non ho mai avuto bisogno di cercare come interagiscono queste regole. Ma sembra che l'ordinamento parziale stia dominando le altre regole di risoluzione del sovraccarico. Quando D non deriva da B, le regole di ordinamento parziale non si applicano e il non modello è più attraente. Quando D deriva da B, l'ordinamento parziale entra in gioco e rende il modello di funzione più attraente, come sembra.

Per quanto riguarda l'ereditarietà che è privata: il codice non richiede mai una conversione da D * a B * che richiederebbe un'eredità pubblica.


Penso che sia qualcosa del genere, ricordo di aver visto un'ampia discussione sugli archivi boost sull'implementazione is_base_ofe sui loop che i contributori hanno attraversato per garantire questo.
Matthieu M.

The exact details are rather complicated- questo è il punto. Spiega per favore. Voglio sapere.
Alexey Malistov

@ Alexey: Beh, pensavo di averti indirizzato nella giusta direzione. Controlla come interagiscono le varie regole di risoluzione del sovraccarico in questo caso. L'unica differenza tra D derivante da B e D non derivante da B rispetto alla risoluzione di questo caso di sovraccarico è la regola di ordinamento parziale. La risoluzione del sovraccarico è descritta in §13 dello standard C ++. Puoi ottenere una bozza gratuitamente: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze

La risoluzione del sovraccarico copre 16 pagine in quella bozza. Immagino che se hai davvero bisogno di capire le regole e l'interazione tra loro per questo caso dovresti leggere la sezione completa §13.3. Non conterei su una risposta qui corretta al 100% e all'altezza dei tuoi standard.
sellibitze

per favore guarda la mia risposta per una spiegazione se sei interessato.
Johannes Schaub - litb

0

Seguendo la tua seconda domanda, nota che se non fosse per const, Host sarebbe mal formato se istanziato con B == D.Ma ​​is_base_of è progettato in modo tale che ogni classe sia una base di se stessa, quindi uno degli operatori di conversione deve essere const.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.