C ++ 11:消除多重继承中的类成员的歧义

时间:2015-02-05 15:57:11

标签: c++ c++11 multiple-inheritance ambiguity enable-if

假设我有这个可变参数的基类模板:

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";
    }
};

foo()成员只有在其模板参数与Base的至少一个参数匹配时才能被调用(此帖子底部列出了Contains的实现) :

Base<int, char>().foo<int>(); // fine
Base<int, char>().foo<void>(); // error

现在我使用 非重叠集 类型定义一个从Base继承两次的派生类:

struct Derived: public Base<int, char>,
                public Base<double, void>
{};

我希望在打电话时如此。

Derived().foo<int>();

编译器会找出要使用的基类,因为它是不包含int的SFINAE。然而,GCC 4.9和Clang 3.5都抱怨模糊的电话。

我的问题是双重的:

  1. 为什么编译器无法解决这种歧义(一般兴趣)?
  2. 无需撰写Derived().Base<int, char>::foo<int>();,我可以做些什么来完成这项工作? 编辑:当我添加两个使用声明时,GuyGreer向我展示了这个调用是消除歧义的。但是,由于我提供了用户继承的基类,因此这不是一个理想的解决方案。如果可能的话,我不希望我的用户必须将这些声明(对于大型类型列表来说可能非常冗长和重复)添加到它们的派生类中。
  3. 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...>
    {};
    

2 个答案:

答案 0 :(得分:14)

这是一个更简单的例子:

template <typename T>
class Base2 {
public:
    void foo(T ) { }
};

struct Derived: public Base2<int>,
                public Base2<double>
{};

int main()
{
    Derived().foo(0); // error
}

原因来自合并规则[class.member.lookup]:

  

否则(即C不包含f的声明或结果声明集为空),S(f,C)为   最初是空的。如果C具有基类,则在每个直接基类子对象Bi中计算f的查找集,   并将每个这样的查找集S(f,Bi)依次合并为S(f,C)    - [..]
   - 否则,如果S(f,Bi)和S(f,C)的声明集不同,则合并是不明确的......

由于我们的初始声明集是空的(Derived中没有方法),我们必须从所有基础合并 - 但我们的基础有不同的集合,因此合并失败。但是,该规则明确仅在CDerived)的声明集为空时才适用。所以为了避免它,我们将它变为非空:

struct Derived: public Base2<int>,
                public Base2<double>
{
    using Base2<int>::foo;
    using Base2<double>::foo;
};

这是有效的,因为应用using的规则是

  

在声明集中, using-declarations 被集合替换   未被派生类(7.3.3)

的成员隐藏或覆盖的指定成员

关于成员是否不同,我们没有评论 - 我们实际上只是在Derived上为foo提供了两次重载,绕过了成员名称查找合并规则。

现在,Derived().foo(0)明确地调用了Base2<int>::foo(int )


除了明确地为每个基地提供using之外,您可以编写一个收集器来完成所有这些:

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
}

在C ++ 17中,您也可以pack expand using declarations,这意味着可以将其简化为:

template <typename... Bases>
struct BaseCollector : Bases...
{
    using Bases::foo...;
};

这不仅可以缩短编写时间,而且编译效率也更高。双赢。

答案 1 :(得分:3)

虽然我无法详细告诉您为什么它不能正常工作,但我已将using Base<int, char>::foo;using Base<double, void>::foo;添加到Derived,现在它编译得很好。

使用clang-3.4gcc-4.9

进行测试