¿Cómo funciona "is_base_of"?

118

¿Cómo funciona el siguiente código?

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. Tenga en cuenta que Bes una base privada. ¿Como funciona esto?

  2. Tenga en cuenta que operator B*()es const. ¿Por qué es importante?

  3. ¿Por qué es template<typename T> static yes check(D*, T);mejor que static yes check(B*, int);?

Nota : Es una versión reducida (se eliminan las macros) de boost::is_base_of. Y esto funciona en una amplia gama de compiladores.

Alexey Malistov
fuente
4
Es muy confuso de su parte usar el mismo identificador para un parámetro de plantilla y un nombre de clase verdadero ...
Matthieu M.
1
@Matthieu M., me encargué de corregir :)
Kirill V. Lyadvinsky
2
Hace algún tiempo escribí una implementación alternativa de is_base_of: ideone.com/T0C1V Sin embargo, no funciona con versiones anteriores de GCC (GCC4.3 funciona bien).
Johannes Schaub - litb
3
Ok, voy a dar un paseo.
jokoon
2
Esta implementación no es correcta. is_base_of<Base,Base>::valuedebería ser true; esto vuelve false.
Chengiz

Respuestas:

109

Si estan relacionados

Supongamos por un momento que en Brealidad es una base de D. Luego, para la llamada a check, ambas versiones son viables porque Hostse pueden convertir en D* y B* . Es una secuencia de conversión definida por el usuario como se describe en 13.3.3.1.2from Host<B, D>a D*y B*respectivamente. Para encontrar funciones de conversión que puedan convertir la clase, las siguientes funciones candidatas se sintetizan para la primera checkfunción de acuerdo con13.3.1.5/1

D* (Host<B, D>&)

La primera función de conversión no es candidata porque B*no se puede convertir a D*.

Para la segunda función, existen los siguientes candidatos:

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

Esos son los dos candidatos a la función de conversión que toman el objeto host. El primero lo toma por referencia constante y el segundo no. Por lo tanto, el segundo es una mejor coincidencia para el *thisobjeto no constante (el argumento del objeto implícito ) por 13.3.3.2/3b1sb4y se usa para convertir a B*para la segunda checkfunción.

Si eliminara la constante, tendríamos los siguientes candidatos

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

Esto significaría que ya no podemos seleccionar por constness. En un escenario de resolución de sobrecarga normal, la llamada ahora sería ambigua porque normalmente el tipo de retorno no participará en la resolución de sobrecarga. Para las funciones de conversión, sin embargo, existe una puerta trasera. Si dos funciones de conversión son igualmente buenas, entonces el tipo de retorno de ellas decide cuál es la mejor según 13.3.3/1. Por lo tanto, si eliminara la constante, se tomaría la primera, porque B*convierte mejor a B*que D*a B*.

Ahora bien, ¿qué secuencia de conversión definida por el usuario es mejor? ¿El de la segunda o primera función de verificación? La regla es que las secuencias de conversión definidas por el usuario solo se pueden comparar si utilizan la misma función de conversión o constructor de acuerdo con 13.3.3.2/3b2. Este es exactamente el caso aquí: ambos usan la segunda función de conversión. Observe que, por tanto, la constante es importante porque obliga al compilador a tomar la segunda función de conversión.

Ya que podemos compararlos, ¿cuál es mejor? La regla es que la mejor conversión del tipo de retorno de la función de conversión al tipo de destino gana (nuevamente por 13.3.3.2/3b2). En este caso, D*convierte mejor a D*que a B*. ¡Así se selecciona la primera función y reconocemos la herencia!

Tenga en cuenta que dado que nunca necesitamos realmente convertir a una clase base, podemos reconocer con ello la herencia privada , porque si podemos convertir de una D*a una B*no depende de la forma de herencia de acuerdo con la4.10/3

Si no están relacionados

Ahora supongamos que no están relacionados por herencia. Así, para la primera función tenemos los siguientes candidatos

D* (Host<B, D>&) 

Y por el segundo ahora tenemos otro conjunto

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

Como no podemos convertir D*a B*si no tenemos una relación de herencia, ¡ahora no tenemos una función de conversión común entre las dos secuencias de conversión definidas por el usuario! Por tanto, seríamos ambiguos si no fuera por el hecho de que la primera función es una plantilla. Las plantillas son la segunda opción cuando hay una función que no es de plantilla que es igualmente buena de acuerdo con 13.3.3/1. Por lo tanto, seleccionamos la función sin plantilla (la segunda) y reconocemos que no hay herencia entre By D!

Johannes Schaub - litb
fuente
2
¡Ah! Andreas tenía el párrafo correcto, lástima que no dio esa respuesta :) Gracias por tu tiempo, me gustaría poder ponerlo como favorito.
Matthieu M.
2
Esta será mi respuesta favorita ... una pregunta: ¿ha leído todo el estándar C ++ o simplemente está trabajando en el comité de C ++? ¡Felicidades!
Marco A.
4
@DavidKernin trabajar en el compromiso de C ++ no te hace saber automáticamente cómo funciona C ++ :) Así que definitivamente tienes que leer la parte del Estándar que se necesita para conocer los detalles, lo cual he hecho. No lo he leído todo, así que definitivamente no puedo ayudar con la mayoría de las preguntas relacionadas con la biblioteca estándar o subprocesos :)
Johannes Schaub - litb
1
@underscore_d Para ser justos, la especificación no prohíbe que std :: traits usen algo de magia del compilador para que los implementadores de bibliotecas estándar puedan usarlos como quieran . Evitarán las acrobacias de las plantillas que también ayudan a acelerar el tiempo de compilación y el uso de la memoria. Esto es cierto incluso si la interfaz se parece a std::is_base_of<...>. Todo está bajo el capó.
Johannes Schaub - litb
2
Por supuesto, las bibliotecas generales boost::necesitan asegurarse de tener estos elementos intrínsecos disponibles antes de usarlos. Y tengo la sensación de que hay una especie de mentalidad de "desafío aceptado" entre ellos para implementar cosas sin la ayuda del compilador :)
Johannes Schaub - litb
24

Averigüemos cómo funciona mirando los pasos.

Empiece por la sizeof(check(Host<B,D>(), int()))pieza. El compilador puede ver rápidamente que se check(...)trata de una expresión de llamada a función, por lo que debe realizar una resolución de sobrecarga check. Hay dos posibles sobrecargas disponibles template <typename T> yes check(D*, T);y no check(B*, int);. Si se elige el primero, obtienes sizeof(yes), elsesizeof(no)

A continuación, veamos la resolución de sobrecarga. La primera sobrecarga es una instanciación de plantilla check<int> (D*, T=int)y el segundo candidato es check(B*, int). Los argumentos reales proporcionados son Host<B,D>y int(). El segundo parámetro claramente no los distingue; simplemente sirvió para convertir la primera sobrecarga en una plantilla. Veremos más adelante por qué la parte de la plantilla es relevante.

Ahora mire las secuencias de conversión que se necesitan. Para la primera sobrecarga, tenemos Host<B,D>::operator D*una conversión definida por el usuario. Para el segundo, la sobrecarga es más complicada. Necesitamos una B *, pero posiblemente haya dos secuencias de conversión. Uno es via Host<B,D>::operator B*() const. Si (y sólo si) B y D están relacionados por herencia será la secuencia de conversión Host<B,D>::operator D*()+ D*->B*existir. Ahora suponga que D de hecho hereda de B. Las dos secuencias de conversión son Host<B,D> -> Host<B,D> const -> operator B* const -> B*y Host<B,D> -> operator D* -> D* -> B*.

Entonces, para B y D relacionados, no check(<Host<B,D>(), int())sería ambiguo. Como resultado, yes check<int>(D*, int)se elige la plantilla . Sin embargo, si D no hereda de B, entonces no check(<Host<B,D>(), int())no es ambiguo. En este punto, la resolución de la sobrecarga no puede ocurrir según la secuencia de conversión más corta. Sin embargo, dadas secuencias de conversión iguales, la resolución de sobrecarga prefiere funciones sin plantilla, es decir no check(B*, int).

Ahora ve por qué no importa que la herencia sea privada: esa relación solo sirve para eliminar la no check(Host<B,D>(), int())resolución de sobrecarga antes de que ocurra la verificación de acceso. Y también ve por qué operator B* constdebe ser constante: de lo contrario, no es necesario el Host<B,D> -> Host<B,D> constpaso, no hay ambigüedad y no check(B*, int)siempre se elegiría.

MSalters
fuente
Su explicación no tiene en cuenta la presencia de const. Si su respuesta es verdadera, entonces constse necesita no. Pero no es cierto. Quitar consty truco no funcionará.
Alexey Malistov
Sin la constante, las dos secuencias de conversión de no check(B*, int)ya no son ambiguas.
MSalters
Si se va solo no check(B*, int), entonces por relacionados By D, no sería ambiguo. El compilador elegiría sin ambigüedades operator D*()realizar la conversión porque no tiene una constante. Es un poco en la dirección opuesta: si elimina la const, introduce una sensación de ambigüedad, pero que se resuelve por el hecho de que operator B*()proporciona un tipo de retorno superior que no necesita una conversión de puntero a B*like D*does.
Johannes Schaub - litb
De hecho, ese es el punto: la ambigüedad está entre las dos secuencias de conversión diferentes para obtener una B*del <Host<B,D>()temporal.
MSalters
Esta es una mejor respuesta. ¡Gracias! Entonces, como entendí, si una función es mejor, pero ambigua, ¿se elige otra función?
user1289
4

El privatebit es completamente ignorado por is_base_ofporque la resolución de sobrecarga ocurre antes de las comprobaciones de accesibilidad.

Puede verificar esto simplemente:

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 mismo se aplica aquí, el hecho de que Bsea ​​una base privada no impide que se lleve a cabo la verificación, solo evitaría la conversión, pero nunca pedimos la conversión real;)

Matthieu M.
fuente
Algo así como. No se realiza ninguna conversión de base. hostse convierte arbitrariamente en D*o B*en la expresión no evaluada. Por alguna razón, D*es preferible en B*determinadas condiciones.
Potatoswatter
Creo que la respuesta está en 13.3.1.1.2 pero todavía tengo que aclarar los detalles :)
Andreas Brinck
Mi respuesta solo explica la parte de "por qué incluso lo privado funciona", la respuesta de sellibitze es ciertamente más completa, aunque estoy esperando ansiosamente una explicación clara del proceso de resolución completa según los casos.
Matthieu M.
2

Posiblemente tenga algo que ver con el ordenamiento parcial de la resolución de sobrecarga. D * es más especializado que B * en el caso de que D se derive de B.

Los detalles exactos son bastante complicados. Debe averiguar las precedentes de varias reglas de resolución de sobrecarga. El pedido parcial es uno. Longitudes / tipos de secuencias de conversión es otro. Finalmente, si dos funciones viables se consideran igualmente buenas, se eligen no plantillas en lugar de plantillas de funciones.

Nunca necesité buscar cómo interactúan estas reglas. Pero parece que el ordenamiento parcial está dominando las otras reglas de resolución de sobrecarga. Cuando D no se deriva de B, las reglas de ordenación parcial no se aplican y la no plantilla es más atractiva. Cuando D se deriva de B, el orden parcial se activa y hace que la plantilla de función sea más atractiva, como parece.

En cuanto a que la herencia sea privada: el código nunca solicita una conversión de D * a B *, lo que requeriría una herencia pública.

sellibitze
fuente
Creo que es algo así, recuerdo haber visto una extensa discusión sobre los archivos de impulso sobre la implementación is_base_ofy los bucles por los que pasaron los contribuyentes para garantizar esto.
Matthieu M.
The exact details are rather complicated- ese es el punto. Por favor explique. Yo quiero saber
Alexey Malistov
@Alexey: Bueno, pensé que te había indicado la dirección correcta. Vea cómo interactúan las distintas reglas de resolución de sobrecarga en este caso. La única diferencia entre D que se deriva de B y D que no se deriva de B con respecto a la resolución de este caso de sobrecarga es la regla de ordenación parcial. La resolución de sobrecarga se describe en §13 del estándar C ++. Puede obtener un borrador gratis: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
La resolución de sobrecarga abarca 16 páginas en ese borrador. Supongo que si realmente necesita comprender las reglas y la interacción entre ellas para este caso, debe leer la sección completa §13.3. No contaría con obtener una respuesta aquí que sea 100% correcta y esté a la altura de sus estándares.
sellibitze
por favor vea mi respuesta para una explicación si está interesado.
Johannes Schaub - litb
0

Siguiendo con su segunda pregunta, tenga en cuenta que si no fuera por const, Host estaría mal formado si se instancia con B == D. Pero is_base_of está diseñado de tal manera que cada clase es una base de sí misma, por lo tanto, uno de los operadores de conversión debe ser const.

Hertz
fuente