C++ 成员函数模板的隐式实例化

C++ Implicit instantiation of member function templates

本文关键字:实例化 成员 函数模板 C++      更新时间:2023-10-16

我想更好地了解编译器何时隐式实例化成员函数模板。

请考虑以下示例:

// example.h
struct Parent {
  virtual void foo(double) {}
};
struct Child : Parent {
  void foo(double d) { bar(d); }
  template<typename T> void bar(T);
  virtual void baz();
}; 

// example.cpp
#include "example.h"
template <typename T> 
void Child::bar(T) {}
void Child::baz() {}

[g++|clang++] -c example.cpp编译它,GCC和clang都隐式实例化函数模板Child::bar<double>。但是,以下看似微小的更改阻止了这一点:

  • 使foo不是虚拟
  • 使Child不继承Parent
  • 删除baz
  • 使baz不是虚拟
  • 使用标头中的声明定义baz

对于隐式实例化何时发生,是否有任何合理简洁的解释,或者我是否需要浏览标准的副本?我已经在SO中搜索了与隐式实例化有关的其他问题,但没有找到太多。我发现最接近的是这个问题,但它是关于类模板(不是成员函数模板)中的虚函数。

需要明确的是:我知道可以通过在标题中包含定义或显式实例化我需要的模板来避免这个问题。这只是在挖掘为什么一个类(我无意中省略了它的显式实例)仍然愉快地编译和链接时的一个好奇心。

我已经把你的代码放到Visual Studio 2013(child.h和child.cpp)中,并尝试了以下方法:

#include "child.h"
Child c1;
c1.bar(10.2);

这会产生一个未解决的外部错误,指示没有发生"隐式实例化"。 表明在这种情况下,Visual Studio和G ++之间存在明显差异。这是通过将 Child::foo 的代码移动到 child.cpp 文件中来修复的。

因此,简短的回答是:您正在经历的在很大程度上是特定于编译器的,对于可移植性,您不应该依赖此行为。根据C++标准,最安全的做法是将模板定义放在 .h(或 .hpp)文件中,或者根据需要显式实例化模板。任何其他管理此问题的方法都可能会在某些编译器中中断。

要进一步了解Visual Studio的行为,请看一下:

  • https://msdn.microsoft.com/en-us/library/aa299373%28v=vs.60%29.aspx
  • https://isocpp.org/wiki/faq/inline-functions#inline-member-fns

意思是,在类的定义中定义的任何函数都是隐式内联的。如果编译器愿意,它可以延迟内联函数的实例化。这意味着当编译 child.obj 时,永远不会创建 child::foo(d),这反过来意味着 bar 永远不会被实例化,因此这会导致链接阶段的编译问题。考虑到foo(double)在技术上是一个虚拟函数,为什么Visual Studio实际上可以做到这一点确实很奇怪,但是Visual Studio似乎将foo()的实例化留到以后使用Child时。所以例如:

Parent *p1 = new Child();

还会导致问题,因为此时编译器尝试创建 Child::foo(double) 并且由于缺少模板定义而无法创建。

从您的结果来看,假设 GCC 将立即实例化内联函数(如果它们是虚拟的)。

您正在经历的行为是多种因素的组合:

  1. 编译器如何处理隐式内联函数
  2. 如何为虚函数创建虚拟表
  3. 以及当编译器需要实例化虚拟成员函数时。

有关详细信息,请参阅以下问题:

  • 什么时候在C++中创建 vtable?
  • "内联"是否隐含在类定义中定义的成员函数C++

所以:

  • 似乎允许编译器延迟内联函数的实例化,直到使用,然后决定是要"内联"它们还是创建一个单独的函数。
  • 在使用类之前,编译器不需要完成对象所需的所有内容的实例化
  • 事实上,即使在创建对象时,编译器也可能没有准备好在代码中为类定义的所有内容。

我决定不详细回答您的具体问题,因为我的研究似乎表明您的代码工作是间接的,并且不应该依赖这种行为。