Como funciona o `is_base_of`?

Como o código a seguir funciona?

typedef char (&yes)[1]; typedef char (&no)[2]; template  struct Host { operator B*() const; operator D*(); }; template  struct is_base_of { template  static yes check(D*, T); static no check(B*, int); static const bool value = sizeof(check(Host(), int())) == sizeof(yes); }; //Test sample class Base {}; class Derived : private Base {}; //Expression is true. int test[is_base_of::value && !is_base_of::value]; 
  1. Note que B é base privada. Como é que isso funciona?

  2. Note que o operator B*() é const. Por que isso é importante?

  3. Por que é template static yes check(D*, T); melhor que static yes check(B*, int); ?

Nota : É versão reduzida (as macros são removidas) de boost::is_base_of . E isso funciona em uma ampla gama de compiladores.

Se eles estão relacionados

Vamos supor por um momento que B é na verdade uma base de D Então, para a chamada check , ambas as versões são viáveis ​​porque o Host pode ser convertido em D* e B* . É uma sequência de conversão definida pelo usuário, conforme descrito por 13.3.3.1.2 do Host para D* e B* respectivamente. Para encontrar funções de conversão que podem converter a class, as seguintes funções candidatas são sintetizadas para a primeira function de check acordo com 13.3.1.5/1

 D* (Host&) 

A primeira function de conversão não é candidata, porque B* não pode ser convertido em D* .

Para a segunda function, existem os seguintes candidatos:

 B* (Host const&) D* (Host&) 

Esses são os dois candidatos de function de conversão que pegam o object host. O primeiro toma por referência const e o segundo não. Assim, o segundo é uma correspondência melhor para o object non-const *this (o argumento de object implícito ) por 13.3.3.2/3b1sb4 e é usado para converter para B* para a segunda function de check .

Se você remover o const, teríamos os seguintes candidatos

 B* (Host&) D* (Host&) 

Isso significaria que não podemos mais selecionar por constness. Em um cenário de resolução de sobrecarga comum, a chamada agora seria ambígua porque normalmente o tipo de retorno não participará da resolução de sobrecarga. Para funções de conversão, no entanto, existe um backdoor. Se duas funções de conversão são igualmente boas, então o tipo de retorno delas decide quem é o melhor de acordo com 13.3.3/1 . Assim, se você remover a const, então a primeira seria tomada, porque B* converte melhor para B* que D* para B* .

Agora, qual sequência de conversão definida pelo usuário é melhor? Aquele para a segunda ou a primeira function de verificação? A regra é que as sequências de conversão definidas pelo usuário só podem ser comparadas se usarem a mesma function de conversão ou construtor de acordo com 13.3.3.2/3b2 . Este é exatamente o caso aqui: ambos usam a segunda function de conversão. Observe que, assim, a const é importante porque força o compilador a assumir a segunda function de conversão.

Desde que podemos compará-los – qual é o melhor? A regra é que a melhor conversão do tipo de retorno da function de conversão para o tipo de destino vence (novamente por 13.3.3.2/3b2 ). Neste caso, D* converte melhor para D* que para B* . Assim, a primeira function é selecionada e reconhecemos a inheritance!

Observe que, como nunca precisávamos realmente converter em uma class base, podemos reconhecer a inheritance privada, porque se podemos converter de um D* para um B* não depende da forma de inheritance de acordo com 4.10/3

Se eles não estão relacionados

Agora vamos supor que eles não estão relacionados por inheritance. Assim, para a primeira function, temos os seguintes candidatos

 D* (Host&) 

E no segundo agora temos outro conjunto

 B* (Host const&) 

Como não podemos converter D* para B* se não tivermos um relacionamento de inheritance, agora não temos nenhuma function de conversão comum entre as duas sequências de conversão definidas pelo usuário! Assim, seríamos ambíguos se não fosse pelo fato de a primeira function ser um modelo. Os modelos são de segunda escolha quando existe uma function não modelo que seja igualmente boa de acordo com 13.3.3/1 . Assim, selecionamos a function não modelo (segunda) e reconhecemos que não há inheritance entre B e D !

Vamos descobrir como isso funciona observando as etapas.

Comece com o sizeof(check(Host(), int())) peça de sizeof(check(Host(), int())) . O compilador pode ver rapidamente que essa check(...) é uma expressão de chamada de function, portanto, ela precisa executar a resolução de sobrecarga na check . Existem duas sobrecargas de candidatos disponíveis, template yes check(D*, T); e no check(B*, int); . Se o primeiro é escolhido, você obtém sizeof(yes) , senão sizeof(no)

Em seguida, vamos ver a resolução de sobrecarga. A primeira sobrecarga é uma check (D*, T=int) instanciação de modelo check (D*, T=int) e a segunda candidata é check(B*, int) . Os argumentos reais fornecidos são Host e int() . O segundo parâmetro claramente não os distingue; ele apenas serviu para tornar a primeira sobrecarga um modelo um. Veremos mais adiante porque a parte do modelo é relevante.

Agora observe as seqüências de conversão necessárias. Para a primeira sobrecarga, temos o Host::operator D* – uma conversão definida pelo usuário. Para o segundo, a sobrecarga é mais complicada. Precisamos de um B *, mas possivelmente há duas seqüências de conversão. Um é via Host::operator B*() const . Se (e somente se) B e D estiverem relacionados por inheritance, a sequência de conversão Host::operator D*() + D*->B* existirá. Agora suponha que D realmente herda de B. As duas seqüências de conversão são Host -> Host const -> operator B* const -> B* e Host -> operator D* -> D* -> B* .

Portanto, para B e D relacionados, no check((), int()) seria ambígua. Como resultado, a yes check(D*, int) do modelo yes check(D*, int) é escolhida. No entanto, se D não herdar de B, no check((), int()) não será ambígua. Neste ponto, a resolução de sobrecarga não pode acontecer com a sequência de conversão mais curta. No entanto, dadas sequências de conversão iguais, a resolução de sobrecarga prefere funções não-modelo, ou seja, no check(B*, int) .

Agora você vê por que não importa que a inheritance seja privada: essa relação serve apenas para eliminar no check(Host(), int()) da resolução de sobrecarga antes que a verificação de access aconteça. E você também vê porque o operator B* const deve ser const: senão não há necessidade do Host -> Host const passo Host -> Host const , sem ambigüidade, e no check(B*, int) sempre ser escolhido.

O bit private é completamente ignorado por is_base_of porque a resolução de sobrecarga ocorre antes das verificações de acessibilidade.

Você pode verificar isso simplesmente:

 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 } 

O mesmo se aplica aqui, o fato de que B é uma base privada não impede que o cheque ocorra, só impediria a conversão, mas nunca pedimos a conversão real;)

Possivelmente tem algo a ver com ordenação parcial resolução de sobrecarga wrt. D * é mais especializado que B * no caso D deriva de B.

Os detalhes exatos são bastante complicados. Você tem que descobrir as precedências de várias regras de resolução de sobrecarga. Ordenação parcial é um deles. Comprimentos / tipos de sequências de conversão é outro. Finalmente, se duas funções viáveis ​​forem consideradas igualmente boas, os não-modelos são escolhidos sobre os modelos de function.

Nunca precisei pesquisar como essas regras interagem. Mas parece que a ordenação parcial está dominando as outras regras de resolução de sobrecarga. Quando D não deriva de B, as regras de ordenação parcial não se aplicam e o não modelo é mais atraente. Quando D deriva de B, a ordenação parcial entra em ação e torna o modelo de function mais atraente – como parece.

Quanto à inheritance sendo privete: o código nunca pede uma conversão de D * para B *, o que exigiria inheritance pública.

Seguindo sua segunda pergunta, note que se não fosse por const, Host seria mal formado se instanciado com B == D. Mas is_base_of é projetado de tal forma que cada class é uma base de si mesmo, portanto, um dos operadores de conversão deve seja const.