消除多重继承中的类成员歧义

Posted

技术标签:

【中文标题】消除多重继承中的类成员歧义【英文标题】:Disambiguate class-member in multiple inheritance 【发布时间】:2015-02-05 15:57:11 【问题描述】:

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

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 的基类中提取出来的。然而,GCC 4.9 和 Clang 3.5 都抱怨一个模棱两可的调用。

我的问题有两个:

    为什么编译器不能解决这种歧义(一般利益)? 不写Derived().Base&lt;int, char&gt;::foo&lt;int&gt;();,我能做些什么来完成这项工作? 编辑: GuyGreer 告诉我,当我添加两个 using 声明时,该调用已消除歧义。但是,由于我为用户提供继承的基类,这不是一个理想的解决方案。如果可能的话,我不希望我的用户必须将这些声明(对于大型类型列表可能非常冗长和重复)添加到他们的派生类中。

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...>
;

【问题讨论】:

这是C++中不同作用域的名称不会重载的一般规则的体现。 【参考方案1】:

这是一个更简单的例子:

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 有基类,计算 f 在每个直接基类子对象 Bi 中的查找集, 并将每个这样的查找集 S(f,Bi) 依次合并为 S(f,C)。 — [..] — 否则,如果 S(f,Bi) 和 S(f,C) 的声明集不同,则合并不明确……

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

struct Derived: public Base2<int>,
                public Base2<double>

    using Base2<int>::foo;
    using Base2<double>::foo;
;

因为应用using 的规则是

在声明集中,using-declarations被集合替换 没有被派生类的成员隐藏或覆盖的指定成员 (7.3.3),

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

现在,Derived().foo(0) 明确调用Base2&lt;int&gt;::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...;
;

这不仅写起来更短,而且编译起来也更高效。双赢。

【讨论】:

谢谢,我明白歧义从何而来。但是,正如我在 GuyGreer 的回答的评论和编辑过的问题中提到的那样,这个解决方案对我的用户不是很友好。我想,如果没有简单的解决方法,我只需要仔细记录。 @JorenHeit 如果您愿意在层次结构中再引入一个类,一个收集一组Base 实例化的类,并让用户从该类继承,您可以让它工作。 @jrok 但是我必须知道 Base 将被实例化的类型,不是吗? @JorenHeit 提供了一个解决方案,可以为您完成所有usings。 @Barry 你不应该把foos 也从BaseCollector&lt;Bases...&gt; 带进来吗?还需要一些东西来阻止递归继承。【参考方案2】:

虽然我无法详细告诉您为什么它不能按原样工作,但我将 using Base&lt;int, char&gt;::foo;using Base&lt;double, void&gt;::foo; 添加到 Derived 并且现在编译良好。

clang-3.4gcc-4.9测试

【讨论】:

这很有趣... +1。但是,如果可能的话,我不希望我的用户在从我提供的基类继承时必须将这些使用声明添加到他们的代码中。不过还是谢谢! @JorenHeit 我不确定为什么从Base 派生两次是有意义的。当然它们不重叠,但为什么不直接从Base&lt;int,char,double,void&gt; 派生? @GuyGreer 我正在为用户提供另一个类,我们称之为Special,它本身是使用保留类型从Base 派生的(对用户隐藏,因此可以保证用户不会使用这种类型,因此不重叠)。如果可以同时从SpecialBase&lt;Whatever...&gt; 派生,那就太好了。 @JorenHeit 您始终可以提供一个派生自SpecialBase&lt;Whatever...&gt;SpecialAndBase&lt;Whatever...&gt;,并添加正确的usings... @T.C.嗯……其实也不错。如此明显......谢谢!

以上是关于消除多重继承中的类成员歧义的主要内容,如果未能解决你的问题,请参考以下文章

C++笔记-类层次结构

继承

具有相同名称的函数的类中的多重继承[重复]

java中的多重继承是啥意思?

多继承 与 多重继承

第54课 被遗弃的多重继承(下)