【问题标题】:Why can't C++ explicitly-instantiated template methods override virtual methods?为什么 C++ 显式实例化的模板方法不能覆盖虚拟方法?
【发布时间】:2013-12-11 08:06:13
【问题描述】:

为什么下面代码中的 TemplateChild 不起作用?我知道虚方法不能是模板,但是为什么显式实例化的模板方法不能覆盖虚方法呢?

#include <iostream>

class VirtBase
{
public:
    VirtBase() {};
    virtual ~VirtBase() {};

    virtual void method( int input ) = 0;
    virtual void method( float input ) = 0;
};

class RegularChild : public VirtBase
{
public:
    RegularChild() {};
    ~RegularChild() {};

    void method( int input ) {
        std::cout << "R" << input << std::endl;
    }
    void method( float input ) {
        std::cout << "R" << input << std::endl;
    }
};

class TemplateBounceChild : public VirtBase
{
public:
    TemplateBounceChild() {};
    ~TemplateBounceChild() {};

    void method( int input ) {
        this->method<>( input );
    }
    void method( float input ) {
        this->method<>( input );
    }
    template< typename INPUT >
    void method( INPUT input ) {
        std::cout << "B" << input << std::endl;
    };
};

class TemplateChild : public VirtBase
{
public:
    TemplateChild() {};
    ~TemplateChild() {};

    template< typename INPUT >
    void method( INPUT input ) {
        std::cout << "T" << input << std::endl;
    };
};

template void TemplateChild::method< int >( int );
template void TemplateChild::method< float >( float );

int main( int, char**, char** )
{
    int i = 1;
    float f = 2.5f;

    VirtBase * v;

    RegularChild r;
    v = &r;

    r.method( i );
    r.method( f );
    v->method( i );
    v->method( f );

    TemplateChild c; // TemplateBounceChild here works correctly.
    v = &c;

    c.method( i );
    c.method( f );
    v->method( i );
    v->method( f );

    return 0;
}

gcc 4.4.7 (CentOS 6) 和 Clang 3.3 (trunk 177401) 同意这两个纯虚方法没有在 TemplateChild 中实现,尽管在编译时 TemplateChild 显式地有一个名为 'method' 的方法,它需要一个float,以及一个名为“method”的方法,它采用 int。

仅仅是因为显式实例化来得太晚,TemplateChild 被认为是非纯虚拟的吗?

编辑:C++11 14.5.2 [temp.mem]/4 表示这不适用于专业化。但是我在 [temp.explicit] 部分中找不到任何关于同一件事的明确内容。

4 A specialization of a member function template does not override a virtual function from a base class.

我还编辑了 TemplateBounceChild 以匹配 C++11 草案的该部分中使用的示例。

【问题讨论】:

    标签: c++ templates virtual-inheritance


    【解决方案1】:

    让我们考虑一下如果允许这样做会发生什么。

    TemplateChild 类的定义可能存在于多个翻译单元(源文件)中。在这些翻译单元中的每一个中,编译器都需要能够为TemplateChild 生成虚函数表(vtable),以确保该虚表存在以供链接器使用。

    vtable 说,“对于动态类型为TemplateChild 的对象,这些是所有虚函数的最终覆盖。”例如,对于RegularChild,vtable 映射两个覆盖RegularChild::method(int)RegularChild::method(float)

    TemplateChild::method 的显式实例化只会出现在一个翻译单元中,编译器只知道它们存在于那个翻译单元中。在编译其他翻译单元时,它不知道存在显式实例化。这意味着你最终会得到两个不同的类的vtables:

    在存在显式实例化的翻译单元中,您将拥有一个映射两个覆盖 TemplateChild::method&lt;int&gt;(int)TemplateChild::method&lt;float&gt;(float) 的 vtable。没关系。

    但是在不存在显式实例化的翻译单元中,您将拥有一个映射到基类虚函数的 vtable(在您的示例中是纯虚函数;让我们假设有基类定义)。

    您甚至可能有两个以上不同的 vtable,例如如果intfloat 的显式实例出现在各自的翻译单元中。

    无论如何,现在我们对同一事物有多个不同的定义,这是一个主要问题。链接器充其量可能会选择一个并丢弃其余的。即使有某种方法可以告诉链接器选择具有显式实例化的链接器,您仍然会遇到编译器可能会去虚拟化虚函数调用的问题,但要做到这一点,编译器需要知道最终的覆盖器是什么(实际上,它需要知道 vtable 中的内容)。

    因此,如果允许这样做,将会出现一些重大问题,并且考虑到 C++ 编译模型,我认为这些问题是无法解决的(至少在 C++ 编译完成方式没有重大变化的情况下无法解决)。

    【讨论】:

    • 您可以在所有 TU 中都有显式的实例化声明(并且只有一个 TU 中有显式的实例化定义)。但这并不能减少出错的可能性。
    • @DyP:好点。 (我之前没有看到任何关于显式实例化声明的内容;它们是 C++11 中的新内容。)我同意这很容易出错,而且我认为使用这样的“功能”太容易了错误。
    • @DyP:这也是我的想法,我没有在这里完全探索,因为我在一个文件中完成了所有操作,而不是拆分它。老实说,我没有意识到您还不能进行显式实例化声明,我假设我能够像其他内联内联代码一样进行内联显式实例化定义。因此,鉴于我假设您可以像使用常规方法一样有效地内联显式实例化,它似乎与使用非模板方法一样安全,因为您同样能够故意双重定义它,但是编译器会注意到。
    【解决方案2】:

    显式实例化只会导致模板被实例化。对于函数模板,这与使用它的效果相同。无论成员函数是显式实例化还是定期使用,都没有理由期望有什么不同的工作方式。

    模板特化不能覆盖非模板函数,因为它们没有相同的名称。特化由包含模板参数的模板 ID 命名。忽略签名中的模板参数,具有不同参数的几个特化可以具有相同的签名。

    如果语言想要确定一个特化应该是一个虚拟覆盖,因为签名与基类虚拟成员一致,它必须确定如果函数是用一些参数调用的,那么所有模板参数都可以推导出来匹配一些虚函数。它不能依赖检查你实际调用函数的方式(由于推论,它看起来像一个虚拟调度),因为你可以使用模板参数以更模糊的方式调用它,或者根本不调用它(这是您尝试使用显式实例化解决的问题)。

    N 个基类 virtual 函数和 M 个可能匹配它们的派生类模板的组合将具有 O(N*M) 复杂度。在这种情况下,特殊功能的扩展性会很差。

    因此,最好在每次实际覆盖时使用常规的virtual 函数声明来明确。 (不过,如果能够将这些函数的别名放在一起,这样它们的地址比较相等可能会很好;尽管指向虚拟成员函数的指针工作方式不同,但相同的函数可能是一个微优化。)

    【讨论】:

    • "如果语言想要确定一个特化应该是一个虚拟覆盖,因为签名与基类虚拟成员一致,它必须确定所有模板参数都可以推导出来,如果函数被调用了一些参数匹配一些虚函数。”这就是显式实例化的重点。我在告诉编译器“这是一个与我想要的覆盖匹配的显式实例化”。如果编译器可以将覆盖推断为模板参数,那就更好了,但我真的没想到这是可行的。
    • @TBBle 该语言没有定义任何隐式模板参数推导做任何显式规范没有做的事情的情况。对于它的价值,您实际上确实使用显式参数列表编写了&lt;int&gt;(int)。语言应该如何确定一个应该是覆盖而不是&lt;true&gt;(int) 的假设重载?
    • template void TemplateChild::method&lt; int &gt;( int );template void TemplateChild::method&lt; true &gt;( int ); 不会冲突吗?如果不是,那么这就是我正在寻找的答案,并且我学到了一些非常有趣的东西。 ^_^
    • @TBBle 不,它们可以共存。但是&lt;true&gt;(int) 不能对应任何可以推断的内容。
    • 我意识到&lt; true &gt;( int ) 不能被推导出来,但是由于它可以与&lt; int &gt;( int ) 共存,那么显然&lt; int &gt; 是名称/参数列表的有效部分,因此无法匹配虚拟方法不包含&lt;&gt;。这符合我从更新的 TemplateBounceChild 中得到的感觉。
    【解决方案3】:

    因为标准是这样说的。参见 C++11 14.5.2 [temp.mem]/3:

    成员函数模板不能是虚拟的。

    【讨论】:

    • 这里的成员函数模板不是虚拟的。 VirtBase::method 是虚拟的,但不是成员函数模板。 TemplateChild::method 是一个函数模板,但不是虚拟的。这才是问题的真正意义所在。
    • 啊,现在我看,TemplateBoundChild::method is 是虚拟的,因为它匹配了它的覆盖的基本方法。 C++11 10.3 [class.virtual]/2.
    • 而 C++11 14.5.2 [temp.mem]/4 实际上明确指出我想要的东西是不允许的。
    • @TBBle:你想要一个成员函数模板来覆盖一个虚函数。为了覆盖一个虚函数,覆盖器必须是虚拟的。
    • 接受这个答案,因为虽然它给了我在问题中所说的一半 (C++11 14.5.2 [temp.mem]/3),但它让我得到了我不知道的一半 (C++11 10.3 [class.virtual]/2)。我还没有看到它不受支持的特别令人信服的理由,但“标准如此”就足够了。
    猜你喜欢
    • 1970-01-01
    • 2011-03-15
    • 2012-11-28
    • 2017-10-06
    • 1970-01-01
    • 2011-05-21
    • 1970-01-01
    • 2020-08-31
    • 1970-01-01
    相关资源
    最近更新 更多