c++ - Desambigua el miembro de la clase en herencia mĂșltiple
c++11 multiple-inheritance (2)
Aquí hay un ejemplo más simple:
template <typename T>
class Base2 {
public:
void foo(T ) { }
};
struct Derived: public Base2<int>,
public Base2<double>
{};
int main()
{
Derived().foo(0); // error
}
La razón de esto proviene de las reglas de combinación [class.member.lookup]:
De lo contrario (es decir, C no contiene una declaración de f o el conjunto de declaraciones resultante está vacío), S (f, C) está inicialmente vacío. Si C tiene clases base, calcule el conjunto de búsqueda para f en cada subobjeto de clase base directa Bi, y combine cada conjunto de búsqueda S (f, Bi) a su vez en S (f, C).
- [..]
- De lo contrario, si los conjuntos de declaración de S (f, Bi) y S (f, C) difieren, la fusión es ambigua ...
Dado que nuestro conjunto de declaración inicial está vacío (
Derived
no tiene métodos), tenemos que fusionarnos desde todas nuestras bases, pero nuestras bases tienen conjuntos diferentes, por lo que la fusión falla.
Sin embargo, esa regla solo se aplica explícitamente si el conjunto de declaraciones de
C
(
Derived
) está vacío.
Entonces, para evitarlo, lo hacemos no vacío:
struct Derived: public Base2<int>,
public Base2<double>
{
using Base2<int>::foo;
using Base2<double>::foo;
};
Eso funciona porque la regla para aplicar
using
es
En el conjunto de declaraciones, el uso de declaraciones se reemplaza por el conjunto de miembros designados que no están ocultos o anulados por miembros de la clase derivada (7.3.3),
No hay comentarios sobre si los miembros difieren o no: efectivamente solo proporcionamos a
Derived
dos sobrecargas en
foo
, sin pasar por las reglas de combinación de búsqueda de nombres de miembros.
Ahora,
Derived().foo(0)
llama inequívocamente a
Base2<int>::foo(int )
.
Alternativamente a tener un
using
para cada base explícitamente, podría escribir un recopilador para hacerlos todos:
template <typename... Bases>
struct BaseCollector;
template <typename Base>
struct BaseCollector<Base> : Base
{
using Base::foo;
};
template <typename Base, typename... Bases>
struct BaseCollector<Base, Bases...> : Base, BaseCollector<Bases...>
{
using Base::foo;
using BaseCollector<Bases...>::foo;
};
struct Derived : BaseCollector<Base2<int>, Base2<std::string>>
{ };
int main() {
Derived().foo(0); // OK
Derived().foo(std::string("Hello")); // OK
}
En C ++ 17, puede
empaquetar expandir
using
declaraciones
también, lo que significa que esto se puede simplificar en:
template <typename... Bases>
struct BaseCollector : Bases...
{
using Bases::foo...;
};
Esto no solo es más corto de escribir, también es más eficiente de compilar. Ganar-ganar
Supongamos que tengo esta plantilla de clase base variadic:
template <typename ... Types>
class Base
{
public:
// The member foo() can only be called when its template
// parameter is contained within the Types ... pack.
template <typename T>
typename std::enable_if<Contains<T, Types ...>::value>::type
foo() {
std::cout << "Base::foo()/n";
}
};
El miembro
foo()
solo se puede llamar cuando su parámetro de plantilla coincide con al menos uno de los parámetros de
Base
(la implementación de
Contains
se enumera en la parte inferior en esta publicación):
Base<int, char>().foo<int>(); // fine
Base<int, char>().foo<void>(); // error
Ahora defino una clase derivada que hereda dos veces de Base, usando conjuntos de tipos no superpuestos :
struct Derived: public Base<int, char>,
public Base<double, void>
{};
Esperaba que al llamar, por ejemplo
Derived().foo<int>();
el compilador descubriría qué clase base usar, porque está SFINAE fuera de la que no contiene
int
.
Sin embargo, tanto GCC 4.9 como Clang 3.5 se quejan de una llamada ambigua.
Mi pregunta es doble:
- ¿Por qué el compilador no puede resolver esta ambigüedad (interés general)?
-
¿Qué puedo hacer para que esto funcione, sin tener que escribir
Derived().Base<int, char>::foo<int>();
? EDITAR: GuyGreer me mostró que la llamada se desambigua cuando agrego dos declaraciones de uso. Sin embargo, dado que estoy proporcionando la clase base para que el usuario herede, esta no es una solución ideal. Si es posible, no quiero que mis usuarios tengan que agregar esas declaraciones (que pueden ser bastante detalladas y repetitivas para listas de tipos grandes) a sus clases derivadas.
Implementación de
Contains
:
template <typename T, typename ... Pack>
struct Contains;
template <typename T>
struct Contains<T>: public std::false_type
{};
template <typename T, typename ... Pack>
struct Contains<T, T, Pack ...>: public std::true_type
{};
template <typename T, typename U, typename ... Pack>
struct Contains<T, U, Pack ...>: public Contains<T, Pack...>
{};
Aunque no puedo decirte en detalle por qué no funciona como es, agregué
using Base<int, char>::foo;
y
using Base<double, void>::foo;
Derived
y se compila bien ahora.
Probado con
clang-3.4
y
gcc-4.9