模板相关

Dependent Name

https://en.cppreference.com/w/cpp/language/dependent_name
在涉及到模板时,如果引用模板参数中的符号,那么这个符号就是dependent name,即依赖于模板实例化才能确定符号类型。

Binding Rules

不依赖模板参数的符号是在模板定义时绑定的。如果绑定时和模板实例化时,同一个符号的含义发生了变化,那程序可能会出问题。

Lookup Rules

依赖模板参数的符号是在模板实例化时才去绑定的。

非ADL

非ADL的情况下,只会在模板定义的上下文寻找符号定义;
下面的例子中,writeObject方法的模板参数类型并不是用户命名空间中定义的,因此对应非ADL场景,只会在模板定义上下文寻找 operator << (std::ostream& os, std::vector&) 符号的定义,不会去用户命名空间中查找:

  1. // an external library
  2. namespace E {
  3. template<typename T>
  4. void writeObject(const T& t) {
  5. std::cout << "Value = " << t << '\n';
  6. }
  7. }
  8. // translation unit 1:
  9. // Programmer 1 wants to allow E::writeObject to work with vector<int>
  10. namespace P1 {
  11. std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) {
  12. for(int n: v) os << n << ' '; return os;
  13. }
  14. void doSomething() {
  15. std::vector<int> v;
  16. E::writeObject(v); // error: will not find P1::operator<<
  17. }
  18. }
  19. // translation unit 2:
  20. // Programmer 2 wants to allow E::writeObject to work with vector<int>
  21. namespace P2 {
  22. std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) {
  23. for(int n: v) os << n <<':'; return os << "[]";
  24. }
  25. void doSomethingElse() {
  26. std::vector<int> v;
  27. E::writeObject(v); // error: will not find P2::operator<<
  28. }
  29. }

ADL

ADL的情况下,不仅会在模板定义的上下文,还会在模板实例化的上下文寻找符号定义;
在下面的这个例子中,模板参数中包括用户命名空间P1中的C,因此对应着ADL场景,会在P1中寻找合适的函数。

  1. namespace P1 {
  2. // if C is a class defined in the P1 namespace
  3. std::ostream& operator<<(std::ostream& os, const std::vector<C>& v) {
  4. for(C n: v) os << n; return os;
  5. }
  6. void doSomething() {
  7. std::vector<C> v;
  8. E::writeObject(v); // OK: instantiates writeObject(std::vector<P1::C>)
  9. // which finds P1::operator<< via ADL
  10. }
  11. }

injected-class-name

https://zh.cppreference.com/w/cpp/language/injected-class-name

非模板情况

在类作用域中,可以直接使用当前类名来指代当前类,这个类名被称为“注入类名”,这个和当前类名相同的符号是在类定义一开始就被自动注入的,注入类名可以被继承,因此private继承可能导致父类的注入类名对子类不可见,此时只能通过使用父类namespace来显式地指代父类;

模板情况

在模板类的作用域中,类名即可指代当前类,又可指代当前模板名称,需要多加分辨。

注入类名与构造函数

在类作用域中,注入类名被当作构造函数的名称,由此引入了一个需要注意的规则:
在限定名C::D解析过程中,如果D是C作用域中的注入类名,且编译器认为C::D可能是一个函数,那么该限定名一定会被解析成构造函数:
不过事实上,只有当D和C同名时,C才会是D作用域的注入类名,毕竟对于D作用域而言,唯一的注入类名就是D了。。也就是说,C::D规则其实就是C::C规则,当然,标准里面的表述方式逻辑上也没毛病,就是理解起来差点意思。

  1. struct A {
  2. // 在A的作用域开始处,编译器会注入符号A作为注入类名
  3. A();
  4. A(int);
  5. template<class T> A(T) {}
  6. };
  7. using A_alias = A;
  8. A::A() {}
  9. A_alias::A(int) {}
  10. template A::A(double);
  11. struct B : A {
  12. using A_alias::A;
  13. };
  14. // 编译器认为A::A可能是一个函数,并且在A的作用域中查找到了注入类名A,于是将A::A解释为指代构造函数
  15. A::A a; // 错误:A::A 被认为指名构造函数,而非类型
  16. // 明确指出A::A是一个struct,编译器不会尝试将A::A解释为构造函数
  17. struct A::A a2; // OK:与 'A a2;' 相同
  18. // B的声明作用域中没有注入类名A,编译器不会将B::A解释为构造函数引用
  19. B::A b; // OK:与 'A b;' 相同

函数相关

ADL - Argument-dependent lookup

https://en.cppreference.com/w/cpp/language/adl
在编写函数调用(包括操作符函数)语句时,如果函数没有限定符并且在当前环境下找不到定义,编译器根据函数参数的限定符去推测函数限定符的行为。

异常相关

Function-try-block

https://en.cppreference.com/w/cpp/language/function-try-block

基础概念

ODR - One Definition Rule

https://en.cppreference.com/w/cpp/language/definition#One_Definition_Rule
一个符号可以被多次声明,但只能定义一次。

ill-formed

非良构。当文档中提及ill-formed时,指的是一个遵从标准的C++编译器应该识别这种情况并给出明显提示。

名字查找

为了编译std::cout << std::endl,编译器进行了:

  • 名字 std无限定的名字查找,找到了头文件 <iostream> 中的命名空间 std 的声明
  • 名字 cout有限定的名字查找,找到了命名空间 std 中的一个变量声明
  • 名字 endl 的有限定的名字查找,找到了命名空间 std 中的一个函数模板声明
  • 名字 operator << 的两个实参依赖查找找到命名空间 std 中的多个函数模板声明,而名字 std::ostream::operator<<有限定名字查找找到声明于类 std::ostream 中的多个成员函数

对于函数和函数模板的名字,名字查找可以将同一个名字和多个声明联系起来,而且可能从实参依赖查找中得到额外的声明。还会进行模板实参推导,并将声明的集合交给重载决议,由它选择所要使用的那个声明。如果适用的话,成员访问的规则只会在名字查找和重载解析之后才被考虑。

有限定的名字查找

限定名,是出现在作用域解析操作符 **::** 右边的名字(参阅有限定的标识符)。 限定名可能代表的是:

  • 类的成员(包括静态和非静态函数、类型和模板等)
  • 命名空间的成员(包括其它的命名空间)
  • 枚举项

**::** 左边为空,则查找过程仅会考虑全局命名空间作用域中作出(或通过 using 声明引入到全局命名空间中)的声明。这样一来,即使局部声明隐藏了该名字,也能够访问它。

在能对 **::** 右边的名字进行名字查找之前,必须完成对其左边的名字的查找(除非左边所用的是 decltype 表达式或左边为空)。对左边的名字所进行的查找,根据这个名字左边是否有另一个 **::** 可以是有限定或无限定的,但其仅考虑命名空间、类类型、枚举和能特化为类型的模板(这一句话的意思参考下面的例子)。

  1. struct A {
  2. static int n;
  3. };
  4. int main() {
  5. int A;
  6. A::n = 42; // 正确:对 :: 左边的 A 的无限定查找忽略变量。因为A在当前作用域中不是类型
  7. A b; // 错误:对 A 的无限定查找找到了变量 A
  8. }

**::** 后跟字符 **~** 再跟着一个标识符(也就是说指定了析构函数或伪析构函数),那么该标识符将在 **::** 左边的名字相同的作用域中查找。下面的例子可以让你喝一壶:

  1. struct C { typedef int I; };
  2. typedef int I1, I2;
  3. extern int *p, *q;
  4. struct A { ~A(); };
  5. typedef A AB;
  6. int main() {
  7. p->C::I::~I(); // ~ 之后的名字 I 在 :: 前面的 I 的同一个作用域中查找
  8. //(也就是说,在 C 的作用域中查找,因此查找结果是 C::I )
  9. q->I1::~I2(); // 名字 I2 在 I1 的同一个作用域中查找,
  10. // 也就是说从当前的作用域中查找,因此查找结果是 ::I2
  11. AB x;
  12. x.AB::~AB(); // ~ 之后的名字 AB 在 :: 前面的 AB 的同一个作用域中查找
  13. // 也就是说从当前的作用域中查找,因此查找结果是 ::AB
  14. }

枚举项

若对左边的名字的查找结果是枚举(无论是有作用域还是无作用域),右边名字的查找结果必须是属于该枚举的一个枚举项,否则程序非良构。

类成员

若对左边的名字的查找结果是某个类、结构体或联合体的名字,则 **::** 右边的名字在该类、结构体或联合体的作用域中进行查找(因此可能找到该类或其基类的成员的声明),但有以下例外情况:

  • 析构函数按如上所述进行查找(即在 :: 左边的名字的作用域中查找)
  • 用户定义转换函数名中的转换类型标识( conversion-type-id ),首先在该类类型的作用域中查找。若未找到,则在当前作用域中查找该名字。
  • 模板实参中使用的名字,在当前作用域中查找(而非在模板名的作用域中查找)
  • using 声明中的名字,还考虑在当前作用域中声明的变量、数据成员、函数或枚举项所隐藏的类或枚举名

**::** 右边所指名的是和其左边相同的类,则右边的名字表示的是该类的构造函数。这种限定名仅能用在构造函数的声明以及引入继承构造函数using 声明中。在所有忽略函数名的查找过程中(即在查找 **::** 左边的名字,或查找详述类型说明符基类说明符中的名字时),则将同样的语法解释成注入类名( injected-class-name ):struct A::A a2; a2类型就是struct A。

有限定名字查找可用来访问被嵌套声明或被派生类隐藏了的类成员。对有限定的成员函数的调用将不再是虚调用

命名空间的成员

**::** 左边的名字代表的是命名空间,或者 **::** 左边为空(这种情况其代表全局命名空间),那么 **::** 右边的名字就在这个命名空间的作用域中进行查找,但有以下例外:

  • 在模板实参中使用的名字在当前作用域中查找
  1. namespace N {
  2. template<typename T> struct foo {};
  3. struct X {};
  4. }
  5. N::foo<X> x; // 错误:X 查找结果为 ::X 而不是 N::X

命名空间 N 中进行有限定查找时,首先要考虑处于 N 之中的所有声明,以及处于 N内联命名空间成员(并且传递性地包括它们的内联命名空间成员)之中的所有声明。如果这个集合中没有找到任何声明,则再考虑在 NN 的所有传递性的内联命名空间成员中发现的所有using 指令所指名的命名空间之中的声明。这条规则是递归实施的:

  1. int x;
  2. namespace Y {
  3. void f(float);
  4. void h(int);
  5. }
  6. namespace Z {
  7. void h(double);
  8. }
  9. namespace A {
  10. using namespace Y;
  11. void f(int);
  12. void g(int);
  13. int i;
  14. }
  15. namespace B {
  16. using namespace Z;
  17. void f(char);
  18. int i;
  19. }
  20. namespace AB {
  21. using namespace A;
  22. using namespace B;
  23. void g();
  24. }
  25. void h()
  26. {
  27. AB::g(); // 在 AB 中查找,找到了 AB::g 并且选择了 AB::g(void)
  28. // (并未在 A 和 B 中查找)
  29. AB::f(1); // 首先在 AB 中查找,未能找到 f
  30. // 然后再在 A 和 B 中查找
  31. // 找到了 A::f 和 B::f(但并未在 Y 中查找,因而不考虑 Y::f)
  32. // 重载解析选中 A::f(int)
  33. AB::x++; // 首先在 AB 中查找,未能找到 x
  34. // 然后再在 A 和 B 中查找。未能找到 x
  35. // 然后再在 X 和 Y 中查找。还是没有 x:这是一个错误
  36. AB::i++; // 在 AB 中查找,未能找到 i
  37. // 然后再在 A 和 B 中查找。找到了 A::i 和 B::i:这是一个错误
  38. AB::h(16.8); // 首先在 AB 中查找:未能找到 h
  39. // 然后再在 A 和 B 中查找。未能找到 h
  40. // 然后再在 X 和 Y 中查找。
  41. // 找到了 Y::h 和 Z::h。重载解析选中 Z::h(double)
  42. }

上面的例子中多次定义同一个符号是违法的,但是同一个声明允许被多次找到:

  1. namespace A { int a; }
  2. namespace B { using namespace A; }
  3. namespace D { using A::a; }
  4. namespace BD {
  5. using namespace B;
  6. using namespace D;
  7. }
  8. void g()
  9. {
  10. BD::a++; // OK : 通过 B 和 D 找到同一个 A::a
  11. }

无限定的名字查找

最内层的外围命名空间

这是英文原文:innermost enclosing namespace 的标准中文表述。
这种表述出现在对友元引入的名字的查找中:
若所查找的是由友元声明所引入的名字:这种情况下仅考虑其最内层的外围命名空间,否则的话,对外围命名空间的查找将照常持续直到全局作用域。
指的是,如果通过friend引入了名字A,当使用A::a时,只会在A所限定的名字空间中查找a,该查找过程不会扩展到A所处的名字空间,这样的一个严格限定的名字空间就叫做a的最内层的外围名字空间(innermost enclosing namespace)。