Como funciona `is_base_of`?

118

Como funciona o código a seguir?

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. Observe que Bé uma base privada. Como é que isso funciona?

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

  3. Porque é template<typename T> static yes check(D*, T); melhor do que static yes check(B*, int);?

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

Alexey Malistov
fonte
4
É muito confuso da sua parte usar o mesmo identificador para um parâmetro de modelo e um nome de classe verdadeiro ...
Matthieu M.
1
@Matthieu M., assumi a responsabilidade de corrigir :)
Kirill V. Lyadvinsky
2
Algum tempo atrás eu escrevi uma implementação alternativa de is_base_of: ideone.com/T0C1V Ele não funciona com versões mais antigas do GCC (GCC4.3 funciona bem).
Johannes Schaub - litb
3
Ok, vou dar um passeio.
jokoon
2
Esta implementação não está correta. is_base_of<Base,Base>::valuedeve ser true; isso retorna false.
chengiz 01 de

Respostas:

109

Se eles são relacionados

Por um momento, vamos supor que Bseja realmente uma base de D. Então, para a chamada para check, ambas as versões são viáveis ​​porque Hostpodem ser convertidas para D* e B* . É uma sequência de conversão definida pelo usuário, conforme descrito por 13.3.3.1.2de Host<B, D>para D*e B*respectivamente. Para encontrar funções de conversão que podem converter a classe, as seguintes funções candidatas são sintetizadas para a primeira checkfunção de acordo com13.3.1.5/1

D* (Host<B, D>&)

A primeira função de conversão não é candidata, porque B*não pode ser convertida para D*.

Para a segunda função, existem os seguintes candidatos:

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

Esses são os dois candidatos à função de conversão que usam o objeto host. O primeiro o considera por referência const e o segundo não. Assim, o segundo é uma combinação melhor para o *thisobjeto não const (o argumento do objeto implícito ) por13.3.3.2/3b1sb4 e é usado para converter para B*para a segunda checkfunção.

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

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

Isso significaria que não podemos mais selecionar por constância. 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 uma porta dos fundos. Se duas funções de conversão forem igualmente boas, o tipo de retorno delas decidirá quem é a melhor de acordo com 13.3.3/1. Portanto, se você remover o const, o primeiro será usado, porque B*converte melhor em do B*que D*em B*.

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

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

Observe que, uma vez que nunca precisamos realmente converter para uma classe base, podemos reconhecer a herança privada porque se podemos converter de umD* para a B*não depende da forma de herança de acordo com4.10/3

Se eles não são relacionados

Agora vamos assumir que eles não estão relacionados por herança. Assim, para a primeira função, temos os seguintes candidatos

D* (Host<B, D>&) 

E para o segundo, agora temos outro conjunto

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

Visto que não podemos converter D*para B*se não tivermos uma relação de herança, agora não temos nenhuma função 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 o fato de que a primeira função é um modelo. Os modelos são a segunda escolha quando há uma função não-modelo que é igualmente boa de acordo com 13.3.3/1. Assim, selecionamos a função não modelo (a segunda) e reconhecemos que não há herança entre Be D!

Johannes Schaub - litb
fonte
2
Ah! Andreas acertou o parágrafo, pena que ele não deu essa resposta :) Obrigado pelo seu tempo, gostaria de poder colocar como favorito.
Matthieu M.
2
Esta vai ser a minha resposta favorita de sempre ... uma pergunta: você leu todo o padrão C ++ ou está apenas trabalhando no comitê C ++ ?? Parabéns!
Marco A.
4
@DavidKernin trabalhando no comitê C ++ não faz você saber automaticamente como funciona o C ++ :) Então você definitivamente tem que ler a parte do Padrão que é necessária para saber os detalhes, o que eu fiz. Não li tudo, então definitivamente não posso ajudar com a maioria das questões relacionadas à biblioteca padrão ou threading :)
Johannes Schaub - litb
1
@underscore_d Para ser justo, a especificação não proíbe o std :: traits de usar alguma mágica do compilador para que os implementadores de biblioteca padrão possam usá-los se quiserem . Eles evitarão as acrobacias de template que também ajudam a acelerar o tempo de compilação e o uso de memória. Isso é verdade mesmo se a interface for assim std::is_base_of<...>. Está tudo sob o capô.
Johannes Schaub - litb
2
Obviamente, as bibliotecas gerais boost::precisam ter certeza de que têm esses intrínsecos disponíveis antes de usá-los. E eu tenho a sensação de que há algum tipo de mentalidade de "desafio aceito" entre eles para implementar coisas sem a ajuda do compilador :)
Johannes Schaub - litb
24

Vamos descobrir como isso funciona observando as etapas.

Comece com a sizeof(check(Host<B,D>(), int()))parte. O compilador pode ver rapidamente que esta check(...)é uma expressão de chamada de função, portanto, ele precisa fazer a resolução de sobrecarga check. Existem duas sobrecargas candidatas disponíveis, template <typename T> yes check(D*, T);eno check(B*, int); . Se o primeiro for escolhido, você obtémsizeof(yes) , senãosizeof(no)

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

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

Portanto, para B e D relacionados, no check(<Host<B,D>(), int())seria ambíguo. Como resultado, o modelo yes check<int>(D*, int)é escolhido. No entanto, se D não herda de B, então no check(<Host<B,D>(), int())não é ambíguo. Neste ponto, a resolução de sobrecarga não pode acontecer com base na seqüência de conversão mais curta. No entanto, sequências de igual conversão dadas, a resolução de sobrecarga prefere funções não-molde, ou seja no check(B*, int).

Você agora vê por que não importa que a herança seja privada: essa relação serve apenas para eliminar a no check(Host<B,D>(), int())resolução de sobrecarga antes que a verificação de acesso aconteça. E você também vê por que operator B* constdeve ser constante: do contrário, não há necessidade da Host<B,D> -> Host<B,D> constetapa, nenhuma ambigüidade e no check(B*, int)sempre seria escolhido.

MSalters
fonte
Sua explicação não leva em conta a presença de const. Se sua resposta for verdadeira, não consté necessário. Mas não é verdade. Remova conste o truque não funcionará.
Alexey Malistov
Sem o const, as duas sequências de conversão para no check(B*, int)não são mais ambíguas.
MSalters
Se você sair apenas no check(B*, int), então para relacionados Be D, não seria ambíguo. O compilador sem ambigüidade escolheria operator D*()realizar a conversão porque não tem um const. É um pouco na direção oposta: se você remover const, introduzirá algum senso de ambigüidade, mas que é resolvido pelo fato de que operator B*()fornece um tipo de retorno superior que não precisa de uma conversão de ponteiro para B*like D*.
Johannes Schaub - litb
Esse é realmente o ponto: a ambigüidade está entre as duas sequências de conversão diferentes para obter um B*do <Host<B,D>()temporário.
MSalters
Esta é uma resposta melhor. Obrigado! Então, como eu entendi, se uma função é melhor, mas ambígua, então outra função é escolhida?
user1289
4

O privatebit é completamente ignorado por is_base_ofporque a resolução da 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 facto de Bser uma base privada não impede a realização da verificação, apenas impediria a conversão, mas nunca pedimos a conversão efectiva;)

Matthieu M.
fonte
Tipo de. Nenhuma conversão de base é realizada. hosté convertido arbitrariamente para D*ou B*na expressão não avaliada. Por alguma razão, D*é preferível B*sob certas condições.
Potatoswatter
Acho que a resposta está em 13.3.1.1.2, mas ainda não resolvi os detalhes :)
Andreas Brinck
Minha resposta apenas explica a parte "por que até mesmo obras privadas", a resposta de sellibitze é certamente mais completa, embora eu esteja aguardando ansiosamente por uma explicação clara do processo de resolução total dependendo dos casos.
Matthieu M.
2

Possivelmente tem algo a ver com o pedido parcial em relação à resolução de sobrecarga. D * é mais especializado do que B * no caso de D derivar 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. A ordenação parcial é uma. Comprimentos / tipos de sequências de conversão são outro. Finalmente, se duas funções viáveis ​​forem consideradas igualmente boas, os não modelos são escolhidos em vez dos modelos de função.

Nunca precisei pesquisar como essas regras interagem. Mas parece que o pedido 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 função mais atraente - como parece.

Quanto ao fato de a herança ser privada: o código nunca pede uma conversão de D * para B * que exigiria herança pública.

Sellibitze
fonte
Acho que é algo assim, lembro-me de ter visto uma extensa discussão sobre os arquivos boost sobre a implementação is_base_ofe os loops pelos quais os contribuidores passaram para garantir isso.
Matthieu M.
The exact details are rather complicated- essa é a questão. Por favor explique. Eu quero saber.
Alexey Malistov
@Alexey: Bem, pensei ter apontado a direção certa. Verifique como as várias regras de resolução de sobrecarga interagem neste caso. A única diferença entre D derivando de B e D não derivando de B com relação à resolução desse caso de sobrecarga é a regra de ordenação parcial. A resolução de sobrecarga é descrita em §13 do padrão C ++. Você pode obter um rascunho gratuitamente: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
A resolução de sobrecarga abrange 16 páginas nesse rascunho. Eu acho, se você realmente precisa entender as regras e a interação entre elas neste caso, você deve ler a seção §13.3 completa. Eu não contaria em obter uma resposta aqui 100% correta e dentro dos seus padrões.
sellibitze
por favor, veja minha resposta para uma explicação disso se você estiver interessado.
Johannes Schaub - litb
0

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

Hertz
fonte