【问题标题】:Use a forward-declared class in a virtual function in a template baseclass where the constructor only needs the forward declare?在模板基类的虚函数中使用前向声明的类,其中构造函数只需要前向声明?
【发布时间】:2013-12-18 22:04:39
【问题描述】:

我正在尝试找出失败的原因。我使用的代码基本上可以浓缩为我下面的内容。我有一个简单的 A 类,我专门用它来制作模板。模板不需要这个类型来编译它的构造函数,并且我实际调用的构造函数(派生类型)没有暴露出来,所以编译器此时无法生成构造函数的代码。

GCC 和 Clang 没有。但是,MSVC (2008 + 2010) 确实尝试编译虚拟成员,因此无法编译。

这是 GCC 和 Clang 的错误,还是 MSVC 的错误?还是我正在进入 UB 领域?

class A;

template <typename X>
class S {
public:
    S() {}

    virtual int useX() { return X::value; }
};

class T : public S<A> {
public:
    T();
};

int main()
{
    new T();
    return 0;
}

【问题讨论】:

  • [temp.inst]/10 “如果虚拟成员函数不会被实例化,则未指定实现是否隐式实例化类模板的虚拟成员函数。”
  • “实现”是什么意思?
  • C++ 标准的实现,即编译器(+链接器)+标准库。

标签: c++ templates visual-c++ gcc


【解决方案1】:

当 MSVC 实例化一个类时,它也会填充它的 vtable,并为此实例化它的所有虚函数,甚至那些从未调用过的虚函数。

在您的情况下,如果没有编译器看到 A 的完整定义,就无法实例化函数 useX

如果您将 useX 声明为非虚拟,则 MSVC 可以正常工作。

似乎这种行为是依赖于编译器的;例如,AIX 在实例化(未使用的)函数方面比 MSVC 更加激进。

【讨论】:

  • 那是调用构造函数的地方,还是定义的地方?
  • 构造函数的一个工作是初始化一个_vtable指针。如果构造函数的编译单元看到(内联)虚函数的定义,它会在 MSVC 中“执行”并尝试实例化该函数。 (总的来说,这是一种很好的方法,因为它允许内联一些对虚函数的调用)。在您的情况下,它会导致编译错误。另一方面,如果删除useX 的主体,编译将通过但链接器将失败。
  • 这就是重点......这个简单程序正在调用的类的构造函数在这里没有定义。它的基本构造函数是,并且该基本构造函数需要的虚函数也是。在该构造函数的翻译单元中,类型必须是已知的,是的。但是为什么要在这里知道呢?
【解决方案2】:

首先,正如许多人所说,该标准完全允许编译器在这种情况下实例化(或选择不实例化)他们心中的内容;通过对标准的严格解释,这是一个代码问题,而不是 MSVC 错误。您所依赖的行为是特定于平台的,因此是非标准的;虽然这本身不是一个“错误”,但它显然是不可移植的。

不过,有趣的是要了解为什么 GCC 和 Clang 与 MSVC 不同,这与每个编译器中 vtable 的设置方式有关。

对象在内存中的外观快速概览:

  1. 没有继承,所有调用都是静态的,没有 vtable 或调整
  2. 单一继承,所有调用都是静态的,巧妙的组织
  3. 多重继承,所有调用都是静态的,指针调整
  4. 虚拟继承,调用可以动态、vtable和指针调整

在第一种情况下,所有信息都可以是静态的;成员函数只是带有隐藏 this 的普通函数。在第二种情况下,派生类(或类,对于一个很长的继承列表)可以巧妙地放在内存中的基类之后,这样 Derived* == Base*。显然,这种优化不适用于多重继承,这意味着每次调用都需要调整 this 指针。然后,虚拟继承添加了一个数组,即 vtable,它在运行时动态选择正确的函数。

这与未使用成员的实例化有什么关系?

一般来说,无论你的实现是什么,你都需要三样东西:

  1. 一个增量,详细说明您关心的子对象在您的 this 中的哪个位置
  2. 虚拟索引 (vindex) 详细说明要调用的虚拟方法
  3. 要调用的方法数组(vtable)

G++ 有一种标准化且复制良好的方式来进行这些调整,用于许多编译器(包括 Clang,因为 Clang 旨在替代 GCC):

  1. 显式存储增量
  2. 还将 vindex 和偏移量计算存储在 vtable 中,在偶数上存储方法地址,在赔率上存储 vindex 的地址

这是一种奇怪的缓存优化,有一些好处;尽管不是最优雅的解决方案,但它已被广泛复制。它在所有情况下都使用一个结构,并且每次都执行相同的计算,具体取决于编译器优化以在适用时用直接调用替换虚拟表跃点(GCC 优化器非常擅长的任务)。

另一方面,MSVC 不仅有点不雅。这是一个可怕的 hack:它对每种情况使用不同的结构。这允许它避免不必要的计算开销,并在许多情况下节省空间。然而,这意味着强制转换成员函数指针会导致它们改变大小,在常见的情况下(派生到基类)会导致它们丢失信息

因此,与更优雅的 GCC 实现不同,MSVC 绝对必须实例化虚成员函数;如果没有,它很容易在翻译单元之间或链接期间丢失信息,并且具有相同的对象由不同大小的结构表示

这是一个他们实际上添加关键字来处理它的问题:http://msdn.microsoft.com/en-us/library/ck561bfk.aspx

因此,虽然您认为编译器不需要该类型,但 MSVC 肯定需要,否则几乎肯定会不安全。

编辑:把自己和简单的单继承弄糊涂了,基类排在第一位:

class A : public B

变成

[[B]A]

所以指针 *B == *A

【讨论】:

    【解决方案3】:

    来自 C++1y 标准草案:(通过 @DyP)

    实现不应隐式实例化函数模板、变量模板、成员 板、非虚拟成员函数、成员类或类模板的静态数据成员 不需要实例化。 未指定实现是否隐式实例化 类模板的虚拟成员函数,如果该虚拟成员函数不会在其他情况下 实例化。 在默认参数中使用模板特化不应导致模板被 隐式实例化,除非类模板可以在需要其完整类型的地方实例化 确定默认参数的正确性。在函数调用中使用默认参数会导致 要隐式实例化的默认参数中的特化。

    强调我的。 template 中的任何 virtual 函数都可以由任何 C++ 编译器在任何时候出于任何原因或没有原因进行实例化,而不会违反 C++ 标准。所以MSVC在这件事上是符合标准的。

    这是 the current draft N3797 中的 14.7.1.11。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-10
      • 1970-01-01
      • 1970-01-01
      • 2019-11-28
      相关资源
      最近更新 更多