14 模板实例化

14.1 按需实例化

C++编译器只在需要获取类大小或者访问类成员时才会对类模板进行实例化:

template<typename T> class C;   // #1 declaration only

C<int>* p = 0;                  // #2 fine: definition of C<int> not needed

template<typename T>
class C {
    public:
        void f();               // #3 member declaration
};                              // #4 class template definition completed

void g (C<int>& c)              // #5 use class template declaration only
{
    c.f();                      // #6 use class template definition;
}                               // will need definition of C::f() in this translation unit

template<typename T>
void C<T>::f()                  // required definition due to #6
{
}

14.2 惰性实例化

C++编译器只实例化需要用到的模板代码。

14.2.1 部分实例化和全部实例化

实例化部分模板定义的过程称为部分实例化:

template<typename T> T f (T p) { return 2*p; }
decltype(f(2)) x = 2;

template<typename T> class Q {
    using Type = typename T::Type;
};
Q<int>* p = 0; // OK: the body of Q<int> is not substituted

template<typename T> T v = T::default_value();
decltype(v<int>) s; // OK: initializer of v<int> not instantiated

14.2.2 类模板成员的实例化

C++编译器在检查模板定义时,总是假设为最好的情况:

// details/lazy1.hpp
template<typename T>
class Safe {
};

template<int N>
class Danger {
    int arr[N];                         // OK here, although would fail for N<=0
};

template<typename T, int N>
class Tricky {
    public:
        void noBodyHere(Safe<T> = 3);   // OK until usage of default value results in an error
        void inclass() {
            Danger<N> noBoomYet;        // OK until inclass() is used with N<=0
        }
        struct Nested {
            Danger<N> pfew;             // OK until Nested is used with N<=0
        };
        union {                         // due anonymous union:
            Danger<N> anonymous;        // OK until Tricky is instantiated with N<=0
            int align;
        };
        void unsafe(T (*p)[N]);         // OK until Tricky is instantiated with N<=0
        void error() {
            Danger<-1> boom;            // always ERROR (which not all compilers detect)
        }
};

当使用Tricky<int, -1> inst实例化上面的类模板时,编译器将只实例化类中的成员函数声明和匿名的联合体,所以anonymousunsafe()会报错。

此外,实例化类模板时还需要虚函数的定义,否则会出现链接错误:

// details/lazy2.cpp
template<typename T>
class VirtualClass {
    public:
        virtual ~VirtualClass() {}
        virtual T vmem(); // Likely ERROR if instantiated without definition
};

int main()
{
    VirtualClass<int> inst;
}

14.3 模板实例化模型

14.3.1 两阶段查找

非依赖型的名称可以在模板解析时就进行查找,这样可以提前发现错误,而依赖型名称只能在提供了模板实参时才能进行查找,所以模板的实例化需要两个阶段:

  1. 在第一阶段,非依赖型名称通过普通查找规则或者ADL规则进行查找,依赖型非受限名称也会被查找,但是不作为最终结果
  2. 在第二阶段,会查找依赖型受限名称,第一阶段中查找过的依赖型非受限名称会再次使用ADL进行查找

第一阶段中查找依赖型非受限名称主要是为了判断该名称是否是模板:

namespace N {
    template<typename> void g() {}
    enum E { e };
}

template<typename> void f() {}

template<typename T> void h(T P) {
    f<int>(p);  // #1
    g<int>(p);  // #2 ERROR
}

int main() {
    h(N::e);    // calls template h with T = N::E
}

f是一个非依赖型名称,编译器通过普通查找规则可以确定f<>是一个模板,而对于g,由于其定义在命名空间N中,其后的<会被解析为小于号运算符,所以在第一阶段就会触发编译错误。

14.3.2 模板实例化代码插入位置

模板实例化代码插入位置(point of instantiation)指将被实参替代后的模板代码插入到源代码中的位置。

  • 函数模板
class MyInt {
    public:
        MyInt(int i);
};

MyInt operator - (MyInt const&);

bool operator > (MyInt const&, MyInt const&);

using Int = MyInt;

template<typename T>
void f(T i)
{
    if (i>0) {
        g(-i);
    }
}
// #1
void g(Int)
{
    // #2
    f<Int>(42); // point of call
    // #3
}
// #4

因为C++不允许在函数定义中再定义函数,所以不能在位置2和位置3插入实例化的f(Int)。因为g(Int)的定义在位置1处不可见,所以实例化的f(Int)不能插入到位置1,只能插入到位置4。

通过这个例子可知,函数模板实例化后的插入位置是在包含引用函数模板的命名空间或者定义之后。原文:

C++ defines the POI for a reference to a function template specialization to be immediately after the nearest namespace scope declaration or definition that contains that reference.

如果将例子中的MyInt更换为int,就会报错:

template<typename T>
void f1(T x)
{
    g1(x); // #1
}

void g1(int)
{
}

int main()
{
    f1(7); // ERROR: g1 not found!
}
// #2 POI for f1<int>(int)

在两阶段查找的第一阶段,因为g1(T)是一个依赖型名称,所以会应用普通查找规则进行查找,但是此时g1(T)定义不可见,因此名字g1(T)是无法解析的。当将实例化的f1<int>(int)的定义插入到位置2时,因为int类型不和命名空间相关联,所以g1(int)无法应用ADL规则进行查找。

  • 类模板
template<typename T>
class S {
    public:
        T m;
};
// #1
unsigned long h()
{
    // #2
    return (unsigned long)sizeof(S<int>);
    // #3
}
// #4

同理,实例化的类模板S<int>不能插入到位置2和位置3。如果将实例化的类模板S<int>插入到位置4,则无法在h()中求出S<int>的大小,所以只能将实例化的S<Int>插入到位置1。

通过这个例子可知,类模板实例化后的插入位置是在包含引用类模板的命名空间或者定义之前。原文:

The POI for a reference to a generated class instance is defined to be the point immediately before the nearest namespace scope declaration or definition that contains the reference to that instance.

在下面的例子中,实例化后的S<char>被插入到位置1,S<double>被插入到位置2a,f(double)被插入到位置2b:

template<typename T>
class S {
    public:
        using I = int;
};

// #1
template<typename T>
void f()
{
    S<char>::I var1 = 41;
    typename S<T>::I var2 = 42;
}

int main()
{
    f<double>();
}
// #2 : #2a , #2b

在一个翻译单元中,可能存在多个实例化后的类模板和函数模板,编译器只保留第一个实例化的类模板,但是保留所有实例化的函数模板,同时不去检查这些函数模板是否是完全一致的。在实际的编译器中,很多编译器简单的将实例化的函数模板插入到翻译单元的最后。

14.3.3 包含模型

C++标准要求模板定义要出现在所有特化和实例化之前,这也就意味着一般要将模板定义在头文件中。

14.4 编译器对模板的支持

如果两个翻译单元中存在相同函数的定义,则在链接过程中就会发生错误,对于模板也存在同样的问题,解决办法包括贪心实例化、查询实例化和迭代实例化。

14.4.1 贪心实例化

编译器在处理每个翻译单元时都会生成函数模板代码,并用特殊的名称进行修饰,这样链接器就能识别重复实例化的函数模板。这种方式主要有三个缺点:

  1. 编译器生成了多份代码,但是最终只保留了一份
  2. 由于编译选项的不同,生成的代码可能是不一样的,但是链接器不进行比较
  3. 目标文件的体积会变大

14.4.2 查询实例化

编译器在所有的翻译单元之间维护一个数据库,编译器通过查询数据库来判断是否需要生成模板代码,不过这种方式没有被市场接受,我也没有想明白这个要怎么做。

14.4.3 迭代实例化

在编译的过程中不实例化模板,并使用预链接器来进行链接。如果发现未实例化的模板,则去重新编译模板代码,重复上述过程直到链接成功。

这种方式主要有三个缺点:

  1. 显著增加链接时间
  2. 处理模板中的编译错误被推迟到了链接阶段,排错成本提高
  3. 需要存储模板定义在源文件中的位置

14.5 显示实例化

在使用模板时,编译器会通过名称查找规则自动找到模板定义并将生成的模板代码插入到合适的位置,这一过程不需要程序员参与,但是C++也支持通过template关键字实现显示的插入生成的模板代码:

template<typename T>
void f(T)
{
}

// four valid explicit instantiations:
template void f<int>(int);
template void f<>(float);
template void f(long);
template void f(char);

14.5.1 手动实例化

为了避免贪心实例化策略带来的问题,可以通过显示实例化的方式实现只在一个翻译单元中进行实例化。这需要只在该翻译单元中提供模板定义,其余翻译单元只提供模板声明,例如将实例化的模板放入后缀为.tpp的源文件中:

// f.hpp:
template<typename T> void f(); // no definition: prevents instantiation
// f.tpp:
#include "f.hpp"
template<typename T> void f() // definition
{
    // implementation
}
// f.cpp:
#include "f.tpp"
template void f<int>(); // manual instantiation

14.5.2 显示实例化声明

通过extern关键字可以声明模板实例化:

// t.hpp:
template<typename T> void f()
{
}

extern template void f<int>();      // declared but not defined
extern template void f<float>();    // declared but not defined
// t.cpp:
template void f<int>();     // definition
template void f<float>();   // definition

有一些特殊情况不适用于显示实例化声明:

  • 内联函数模板
  • auto或者decltype(auto)类型的变量,以及返回类型为auto的函数模板
  • 和常量表达式关联的模板
  • 引用类型的变量
  • 类模板和别名模板

14.6 编译时if

通过编译时if可以实现只实例化部分分支,例如在下面的例子中,else分支不会被实例化:

template<typename T> bool f(T p) {
    if constexpr (sizeof(T) <= sizeof(long long)) {
        return p>0;
    } else {
        return p.compare(0) > 0;
    }
}

bool g(int n) {
    return f(n); // OK
}

以往函数模板是作为一个整体实例化的,但是在支持编译时if后,编译器必须支持部分实例化模板。原文:

However, it requires implementations to refine the unit of instantiation: Whereas previously function definitions were always instantiated as a whole, now it must be possible to inhibit the instantiation of parts of them.

14.7 标准库中的显示实例化

标准库中提供的某些模板一般只会被特定的类型实例化,其中就用到了显示实例化声明,例如std::basic_string<char>(也就是std::string):

namespace std {
    template<typename charT, typename traits = char_traits<charT>,
                typename Allocator = allocator<charT>>
    class basic_string {
        // ...
    };

    extern template class basic_string<char>;
    extern template class basic_string<wchar_t>;
}

14.8 后记

results matching ""

    No results matching ""