通过 stateful constexpr,我们希望实现以下功能
1 | int main () { |
通过在编译器实现副作用,可以在编译期实现容器、查看某个类是否已经被实例化、查看一个函数在决议后的地址,甚至实现反射。
我们还能看到:
- 友元的一个有趣的应用
- Argument-dependent lookup
- C++ 模板实例化规则
实现f
f
的实现如下
1 | template< |
其中noexcept (X)
用来检测里面的X
是否是常量表达式。特别地,在这里,如果函数flag
没有被定义,那么noexcept
会返回false
。容易想到,下面对dependent_writer<B>
求sizeof
应该是为了去定义flag
函数。下面我们来看这个类模板的实现。
1 | constexpr int flag (int); // 1: flag只声明而未定义 |
这段代码的核心点是,我们延迟了函数 flag
到实例化 writer<Tag>
的时候。其方式是声明函数 flag
是 writer
的友元,那么其定义应当随 writer<Tag>
的模板实例化进行。
现在,一个最直接的疑问点是为什么不直接使用 writer<Tag>
,这个涉及到在附注中列举的 C++ 模板实例化的一些规则。其原因是使用裸的 writer<int>
会直接实例化。为了避免这个问题,我们需要延迟这个实例化到 f
的实现上,因此引入 dependent_writer
,它要依赖一个 bool B
才能实例化。
附注
友元函数
因为友元函数没有 this 指针,则参数要有三种情况:
- 要访问非 static 成员时,需要对象做参数;
- 要访问 static 成员或全局变量时,则不需要对象做参数;
- 如果做参数的对象是全局对象,则不需要对象做参数
可以直接调用友元函数,不需要通过对象或指针
unqualified name lookup
介绍 ADL 即 Argument-dependent lookup 之前,先介绍这个。
一个 unqualified name 是没有出现在 scope resolution operator 即 ::
右边的名字。
unqualified name lookup 会查找如下的 scope,直到找到至少一个 declaration。此时,查找停止,并且不再进一步查找别的 scope 了。
- File scope
- Namespace scope
- Definition outside of its namespace
- Non-member function definition
- Class definition
- TODO
Note: lookup from some contexts skips some declarations, for example, lookup of the name used to the left of :: ignores function, variable, and enumerator declarations, lookup of a name used as a base class specifier ignores all non-type declarations
For the purpose of unqualified name lookup, all declarations from a namespace nominated by a using
directive appear as if declared in the nearest enclosing namespace which contains, directly or indirectly, both the using
-directive and the nominated namespace.
最后讲到,Unqualified name lookup of the name used to the left of the function-call operator (and, equivalently, operator in an expression) is described in argument-dependent lookup,也就是我们马上要提及的 ADL。
ADL
Argument-dependent lookup, also known as ADL, or Koenig lookup, is the set of rules for looking up the unqualified function names in function-call expressions, including implicit function calls to overloaded operators. These function names are looked up in the namespaces of their arguments in addition to the scopes and namespaces considered by the usual unqualified name lookup.
也就是说会根据函数的参数所在的名字空间,去查找函数。
如下所示,小括号会阻止 ADL。
1 | int main() |
C++ 模板实例化的规则
模板实例化与模板特化
**模板实例化(instantiation)**指通过指定的模板参数实例化一个模板。此时(point of instantiation),编译器会为对应的模板特化生成real code。1
2
3
4
5template <typename T>
struct test{ T m; };
template test<int>; // explicit instantiation
test<int> a; // implicit instantiation**模板特化(specialization)**依然是一个模板,它仍然需要进行实例化来得到real code。
1
2
3template <typename T>
struct test{ T m; };
template <> struct test<int>{ int newM; } // specialization类比到函数上,实例化类似于调用,特化类似于重载(虽然特化还可以分为全特化和偏特化)
point of instantiation
一旦在一个需要实例化的上下文中refer了一个template specialization,就会产生一个point of instantiation,此时编译器就会为这个template specialization生成代码。
下面作者的话有点难于理解,于是我直接参考标准temp.point理解,并且问了问题。对于(member) function template specializationX
,如果他被另一个template specializationY
所refer,且这个Y
依赖于某个模板参数,那么他的point of instantiation等同于Y
。在同样的情况下,如果X
是class template specialization,那么它应该在Y
紧前面初始化。
对于其他的情况,设X
处在的名字空间的声明/定义位于D
,如果X
是一个函数,在D
紧后面初始化;如果X
是一个类,那么在D
紧前面初始化。
function template specialization可以有任意多次的实例化,但我们必须保证它们的结果是相同的,否则ill-formed。可以联想到inline函数也是这样的。如果class template specialization含有友元声明,那么它的所有友元将被对待为仿佛一个explicit specialization在point of instantiation处已经被声明。
class template specialization只能有一次的实例化。隐式实例化function template specialization的条件
没有被显式特化或显式实例化。
上下文需要它的定义。隐式实例化class template specialization的条件
没有被显式特化或显式实例化。
上下文需要completely-defined的type。
特别地,隐式实例化class template specialization只会实例化成员的声明而非定义,除非:- 成员是static data member,此时不需要实例化。这是因为禁止在声明时同时初始化非const的static类成员。
- 成员是unscoped enumeration or an anonymous union,此时声明+定义
一个class template specialization的成员的定义是在它被需要时才被隐式实例化。
Q:是否可以认为当我们没有refer到writer
时,它所有的成员的声明都没有被实例化?
Q:是否可以认为当我们没有refer到writer
的友元成员函数f
时,f
没有被实例化?