13-实例化

On-Demand 实例化(隐式实例化)

  • 编译器遇到模板特化时会用所给的实参替换对应的模板参数,从而产生特化
  • 如果声明一个指向某类型的指针或引用,不需要看到类模板定义,但如果要访问特化的成员或想知道模板特化的大小,就要先看到模板定义
template<typename T>
struct A; // 前置声明

A<int>* p = 0; // OK:不需要 A<int> 的定义

template<typename T>
struct A {
  void f();
};

void g(A<int>& a) { // 只使用类模板声明
  a.f(); // 使用了类模板定义,需要 A::f() 的定义
}

template<typename T>
void A<T>::f() { // A::f() 的定义
}
  • 函数重载时,如果候选函数的参数是类类型,则该类必须可见。如果重载函数的参数是类模板,为了检查重载匹配,就需要实例化类模板
template<typename T>
struct A {
  A(int);
};

void f(A<double>) {}
void f(int) {}

int main() {
  f(42); // 前两个函数声明都可以匹配,尽管只调用第二个,仍会实例化第一个
}

延迟实例化(Lazy Instantiation)

  • 实例化只会对需要的部分进行。隐式实例化模板时也实例化了每个成员声明,但没有实例化定义。但匿名 union 和虚函数例外,如果类模板包含一个匿名 union,则 union 定义的成员也被实例化了,而虚函数是否实例化依赖于具体实现
  • 实例化模板时,只有当函数用上了默认实参时才会实例化该实参
template<typename T>
class Safe {
};

template<int N>
class Danger {
  int arr[N]; // N<=0 则失败,但编译器会假设 N 是正整数
};

template<typename T, int N>
class Tricky {
 public:
  void noBodyHere(Safe<T> = 3); // 不一定能用整数对模板 Safe 初始化
  // 但编译器会假设对 Safe<T> 的泛型定义不会用到这个默认实参
  void inclass() {
    Danger<N> noBoomYet; // OK until inclass() is used with N<=0
  }
  void error() { // 会引发错误
    Danger<-1> boom;
  }
  // Danger<-1> 会被要求给出类 Danger<-1> 的完整定义
  // 于是会定义一个 -1 大小的数组
  // 即使 error 没被使用不被实例化,也仍会引发错误
  void unsafe(T (*p)[N]); // 在 N 没被模板参数替换前该声明不会出错
  T operator->();
  // virtual Safe<T> suspect();
  struct Nested {
    Danger<N> pfew; // OK until Nested is used with N<=0
  };
  union { // 匿名 union
    int align;
    Safe<T> anonymous;
  };
};

int main() {
  Tricky<int, 0> ok; // 默认构造函数和析构函数肯定会被调用
  // 因此它们的定义必须存在,虚函数的定义也必须存在
  // 因此 suspect() 只有声明没有定义则会出现链接错误

  // 对于 inclass() 和结构 Nested 的定义,会要求一个 Danger<0> 类型
  // 但因为没有用到这两个成员的定义,因此不会产生定义而引发错误

  // 所有成员声明都会被生成,因此 N 为 0 时 unsafe(T (*p)[N]) 会产生错误
  // 同理,如果匿名 union 中的不是 Safe<T> 而是 Danger<T> 也会产生错误

  // 对于 operator-> 通常应该返回指针类型,或用于这个操作符的 class 类型
  // 但在模板中规则会更灵活,虽然这里 T 为 int,返回 int 类型,但不会出错
}

两阶段查找(Two-Phase Lookup)

  • 编译器解析模板时不能解析依赖型名称,所以编译器会在 POI(point of instantiation,实例化点)再次查找依赖型名称,而非依赖型名称在首次看到模板时就会进行查找。因此就有了两阶段查找:第一阶段发生在模板解析阶段,第二阶段在模板实例化阶段
    • 第一阶段使用普通查找规则(适当情况也会用 ADL)查找非依赖型名称和非受限的依赖型名称(如函数调用中的函数名称,该名称具有依赖型实参所以是依赖型名称),但后者的查找不完整,在实例化时还会再次查找
    • 第二阶段发生的地点称为 POI,这个阶段会查找依赖型受限名称,并对非受限的依赖型名称再次进行 ADL

POI(Points of Instantiation)

  • 编译器会在模板中的某个位置访问模板实体的声明或定义,实例化相应的模板定义时就会产生 POI,POI 是代码中的一个点,在该点会插入替换后的模板实例
struct A {
  A(int i);
};

A operator-(const A&);
bool operator>(const A&, const A&);

using Int = A;

template<typename T>
void f(T i) {
  if (i > 0) g(-i);
}

// (1) 若此处为 POI,g() 不可见,g(-i) 不能被解析
void g(Int) {
  // (2) 不能是POI,不允许在此处插入 ::f<Int>(Int) 的定义
  f<Int>(42); // 调用点
  // (3) 同 (2)
}
// (4) 此时 g() 可见,POI:实例化 ::f<Int>(Int)
  • 这里使用类型 A 而不是 int 的原因是,POI 执行第二次查找(查找 g(-i))使用了 ADL,int 没有关联命名空间,不会发生 ADL,也就找不到函数 g()
  • 类模板实例的 POI 位置不同,它只能定义在包含该实例的声明(或定义)前的最近作用域
template<typename T>
struct A {
  T x;
};
// (1) POI
unsigned long f() {
  // (2) 不能是 POI,A<int> 的定义不能出现在函数作用域内
  return (unsigned long)sizeof(A<int>);
  // (3) 同 (2)
}
// (4) 如果此处是 POI,则 sizeof(A<int>) 无效,因为编译后才知道大小
  • 实例化一个模板时还可能附带实例化其他模板
template<typename T>
struct A {
  using Type = int;
};

// (1) A<char> 的 POI
template<typename T>
void f() {
  A<char>::Type a = 41;
  typename A<T>::Type b = 42;
}

int main() {
  f<double>();
}
// (2) f<double> 的 POI,但因为 f 使用了依赖型名称 A<T>,还需要一个二次 POI
// 因此此处有两个 POI,对于类实体,二次 POI 位于主 POI 之前(函数实体则位置相同)
// (2)(a) A<double> 的 POI;(2)(b) f<double> 的 POI
  • 一个编译单元通常会包含一个实例的多个 POI,对类模板实例,每个编译单元只有首个 POI 会被保留,其他 POI 会被忽略(它们不会被真正认为是 POI),对于函数模板和变量模板的实例,所有 POI 都会保留。ODR 原则要求在保留的任何一个 POI 位置出现的实例化体等价,但编译器没有这个约束,因此编译器允许选择一个 non-class 类型的 POI 执行实例化,而不用担心其他 POI 产生不同的实体

显式实例化

  • 为模板特化显式生成 POI 的构造称为显式实例化指示符,它由 template 关键字和特化声明组成
template<typename T>
void f(T) {}

template void f<int>(int); // 实例化 f<int>(int),无实参推断
template void f<>(float); // 实例化 f<float>(float),有实参推断
template void f(long); // 实例化 f<long>(long),有实参推断
  • 类模板成员也可以显式实例化,显式实例化一个类也会实例化所有成员
template<typename T>
class A {
 public:
  void f() {}
};

template class A<void>; // 同时显式实例化 template void A<void>::f()
template void A<int>::f(); // 仅显式实例化该成员
  • 显式实例化后,不能再特化实例化过的版本
template<typename T>
void f() {
}

template void f<int>(); // 显式实例化

template<> // 错误:已经基于int实例化过,不能再对int特化
void f<int>() {
  std::cout << 1;
}
  • 但特化过后可以显式实例化,实例化的是特化版本
#include <iostream>

template<typename T>
void f() {
}

template<>
void f<int>() {
  std::cout << 1;
}

template void f<int>(); // 显式实例化的是特化的定义

int main() {
  f<int>(); // 1
}
  • 显式实例化不会影响类型推断规则,它只是实例化了一个实体,并不是一个可以优先匹配的非模板函数。从函数模板实例化而来的函数永远不和普通函数等价
template<typename T>
void f(T, T) {}

template void f<double>(double, double); // 显式实例化

f(1, 3.14); // 错误:推断类型不一致,不存在可匹配的普通函数 void f(double, double)
f<double>(1, 3.14); // OK

显式实例化声明(Explicit Instantiation Declarations)

  • 由关键字 extern 指定的显式实例化称为显式实例化声明,它会抑制隐式实例化,但以下情况例外:
    • 内联函数为了展开内联,仍能被实例化
    • auto 或 decltype(auto) 类型变量和函数返回类型,仍可以被实例化来确定类型
    • 值为常量表达式的变量仍能被实例化以计算它们的值
    • 引用类型变量仍能被实例化,这样引用的实体才能被解析
    • 类模板和别名模板,为了检查生成类型仍能被实例化
  • 使用显式实例化声明,可以在头文件中提供模板定义,以此抑制隐式实例化的特化。每个显式实例化声明必须与定义配对,省略定义将引发链接错误
// t.hpp:
template<typename T> void f() {}

extern template void f<int>(); // 声明但不定义
extern template void f<float>(); // 声明但不定义

// t.cpp:
template void f<int>(); // 定义
template void f<float>(); // 定义
  • 当特化被用在许多不同的编译单元中,显式实例化声明能用来提高编译效率。不同于手动实例化每需要一个新的特化都要手动更新显式实例化定义列表,显式实例化声明能在任何情况下作为一个优化引入。然而编译时间优化上就不如手动实例化了,因为可能产生一些冗余的隐式实例化,且模板定义也会被作为头文件的一部分解析

标准库中的显式实例化

  • 标准库就有使用显式实例化的例子,如 basic_iostream 常用于char 或 wchar_t,标准库的实现就会为这些常见情况引入显式实例化声明
// VS2019 头文件 <istream> 中的部分源码

#if defined(_DLL_CPPLIB)

#if !defined(_CRTBLD) || defined(__FORCE_INSTANCE)
template class _CRTIMP2_PURE_IMPORT basic_iostream<char, char_traits<char>>; // __declspec(dllimport)
template class _CRTIMP2_PURE_IMPORT basic_iostream<wchar_t, char_traits<wchar_t>>;
#endif // !defined(_CRTBLD) || defined(__FORCE_INSTANCE)

#ifdef __FORCE_INSTANCE
template class _CRTIMP2_PURE_IMPORT basic_iostream<unsigned short, char_traits<unsigned short>>;
#endif // __FORCE_INSTANCE
#endif // defined(_DLL_CPPLIB)

if constexpr

  • C++17 引入了编译期 if,条件为 false 的分支会被丢弃而不会实例化
template<typename T, typename... Ts>
void print(T&& t, Ts&&... ts) {
  std::cout << t << std::endl;
  if constexpr (sizeof...(ts) > 0) {
    print(std::forward<Ts>(ts)...); // 当条件满足时才实例化
  }
}

int main() {
  print(3.14, 42, std::string{ "hello" }, "world");
}
  • C++17 之前没有 if constexpr,需要用特化或重载实现类似的功能
template<bool b>
struct A;

template<typename T, typename... Ts>
void print(T&& t, Ts&&... ts) {
  std::cout << t << std::endl;
  A<(sizeof...(ts) > 0)>::f(std::forward<Ts>(ts)...);
}

template<bool b>
struct A {
  template<typename... Ts>
  static void f(Ts&&... ts) {
    print(std::forward<Ts>(ts)...);
  }
};

template<>
struct A<false> {
  template<typename... Ts>
  static void f(Ts&&... x) {}
};

int main() {
  print(3.14, 42, std::string{ "hello" }, "world");
}
  • if constexpr 能用于任何函数而不仅局限于模板,但判断的必须是编译期表达式
void f();

void g() {
  if constexpr (sizeof(int) == 1) {
    f(); // f() 未定义也不会报错
  }
}

文章作者: 张小飞
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 张小飞 !
 上一篇
14-模板实参推断 14-模板实参推断
推断的过程 每个实参-参数对的推断都是独立的,如果结果矛盾推断就会失败 template<typename T> const T& max(const T& a, const T& b) { return
下一篇 
12-模板中的名称 12-模板中的名称
ADL(Argument-Dependent Lookup,Koenig Lookup) 下面例子解释了名称查找的基本规则 namespace A { struct X; struct Y; void f(int); void g(X);
  目录