关于C++17标准[temp.point]/4中模板参数相关上下文的含义问询
先把你提到的标准原文贴出来:
将根据C++17标准[temp.point]/4(笔者加粗部分):对于类模板特化、类成员模板特化或类模板的类成员特化,若该特化因在另一模板特化内被引用而被隐式实例化,且引用该特化的上下文依赖于模板参数,同时该特化在封闭模板实例化之前未被实例化,则其实例化点紧接在封闭模板的实例化点之前。否则,此类特化的实例化点紧接在引用该特化的命名空间作用域声明或定义之前。
我来一步步拆解这条规则,结合实例帮你搞清楚每个关键点:
先明确核心概念
这里的"context" 和你之前了解的「定义上下文」(模板本身被编写的源码位置)、「实例化上下文」(触发模板实例化的源码位置)是同源的,本质就是引用该特化的具体源码位置——用来确定实例化特化时,哪些名称、特化、重载是可见的。
"上下文依赖于模板参数" 的核心判断标准:这个引用的行为会随着封闭模板的参数变化而变化。简单说就是:你引用的特化是用封闭模板的模板参数来实例化的(比如在模板
B<T>里写A<T>,T是B的模板参数),而不是一个固定的、和模板参数无关的特化(比如在B<T>里写A<int>,int是固定类型,和T没关系)。
场景一:满足"上下文依赖模板参数"的情况
当同时满足三个条件时:
- 特化是因为在另一个模板的特化里被引用,才触发隐式实例化的;
- 引用它的上下文依赖封闭模板的参数;
- 该特化在此之前没有被实例化过;
此时这个特化的实例化点,会紧接在封闭模板特化的实例化点之前。
举个直观的例子:
// 主模板A template<typename T> struct A { void func() {} }; // 模板B,里面引用A<T>——这里的T是B的模板参数,引用上下文依赖T template<typename T> struct B { A<T> member; }; // 触发B<int>的实例化 B<int> b;
在这个例子里:
A<int>是因为B<int>的实例化被引用,才触发隐式实例化的;- 引用
A<T>的上下文在B<T>内部,依赖T(换个T就对应不同的A特化); - 之前没有其他地方实例化过
A<int>;
所以A<int>的实例化点,就在B<int>的实例化点紧前位置——也就是B<int> b;这行代码的实例化点之前。
场景二:不满足"上下文依赖模板参数"的情况
只要三个条件中有一个不满足(比如引用上下文不依赖模板参数,或者该特化已经被实例化过),那么特化的实例化点就会紧接在引用它的那个命名空间作用域的声明/定义之前。
举两个典型子例:
子例1:引用上下文在命名空间作用域(无模板参数)
template<typename T> struct A { void func() {} }; // 在命名空间作用域直接引用A<X>,上下文不依赖任何模板参数 struct X {}; A<X> global_a; // 模板B里引用A<X>,这里X是固定类型,和B的参数T无关 template<typename T> struct B { A<X> member; }; B<double> b;
这里A<X>的第一次引用是global_a的定义,上下文是命名空间作用域,不依赖任何模板参数,所以A<X>的实例化点紧接在global_a的定义之前。后面B<double>里的A<X>已经被实例化过了,不会触发新的实例化点。
子例2:引用在模板内但不依赖参数
template<typename T> struct A { void func() {} }; // 模板B里引用A<int>,int是固定类型,和B的参数T无关 template<typename T> struct B { A<int> member; }; B<double> b;
这里A<int>的引用上下文虽然在模板B内部,但不依赖T——不管T是什么,引用的都是同一个A<int>。所以A<int>的实例化点紧接在B的模板定义之前(因为这是第一次引用它的命名空间作用域位置)。
为什么这个规则重要?实例化点直接影响代码行为
实例化点的核心作用是:确定实例化特化时,哪些名称、特化、重载是可见的,直接决定代码的运行结果。比如:
#include <iostream> template<typename T> struct A { void print() { std::cout << "主模板\n"; } }; // 模板foo里引用A<T>,上下文依赖T template<typename T> void foo() { A<T> a; a.print(); } // 特化A<int> template<> struct A<int> { void print() { std::cout << "int特化版\n"; } }; int main() { foo<int>(); // 输出:int特化版 }
这里A<int>的实例化点紧接在foo<int>的实例化点之前(也就是main里调用foo<int>()的位置之前),此时A<int>的特化已经定义好了,所以会使用特化版。
如果把特化移到main之后:
#include <iostream> template<typename T> struct A { void print() { std::cout << "主模板\n"; } }; template<typename T> void foo() { A<T> a; a.print(); } int main() { foo<int>(); // 输出:主模板 } template<> struct A<int> { void print() { std::cout << "int特化版\n"; } };
此时A<int>的实例化点在foo<int>的实例化点之前,而foo<int>的实例化点在main的调用处,这时特化还没定义,所以只能用主模板。
但如果foo里引用的是A<int>(不依赖T):
#include <iostream> template<typename T> struct A { void print() { std::cout << "主模板\n"; } }; template<typename T> void foo() { A<int> a; // 不依赖T a.print(); } int main() { foo<double>(); // 输出:主模板 } template<> struct A<int> { void print() { std::cout << "int特化版\n"; } };
这里A<int>的实例化点紧接在foo的模板定义之前,那时特化还没出现,所以即使特化在main之后,也不会被用到,输出主模板。
内容的提问来源于stack exchange,提问作者Brian Bi

