学习C++ - 不常见概念解释
模板相关
1. Dependent Name
https://en.cppreference.com/w/cpp/language/dependent_name
在涉及到模板时,如果引用模板参数中的符号,那么这个符号就是dependent name,即依赖于模板实例化才能确定符号类型。
1.1. Binding Rules
不依赖模板参数的符号是在模板定义时绑定的。如果绑定时和模板实例化时,同一个符号的含义发生了变化,那程序可能会出问题。
1.2. Lookup Rules
依赖模板参数的符号是在模板实例化时才去绑定的。
1.2.1. 非ADL
非ADL的情况下,只会在模板定义的上下文寻找符号定义;
下面的例子中,writeObject方法的模板参数类型并不是用户命名空间中定义的,因此对应非ADL场景,只会在模板定义上下文寻找 operator << (std::ostream& os, std::vector
1 | // an external library |
1.2.2. ADL
ADL的情况下,不仅会在模板定义的上下文,还会在模板实例化的上下文寻找符号定义;
在下面的这个例子中,模板参数中包括用户命名空间P1中的C,因此对应着ADL场景,会在P1中寻找合适的函数。
1 | namespace P1 { |
2. injected-class-name
https://zh.cppreference.com/w/cpp/language/injected-class-name
2.1. 非模板情况
在类作用域中,可以直接使用当前类名来指代当前类,这个类名被称为“注入类名”,这个和当前类名相同的符号是在类定义一开始就被自动注入的,注入类名可以被继承,因此private继承可能导致父类的注入类名对子类不可见,此时只能通过使用父类namespace来显式地指代父类;
2.2. 模板情况
在模板类的作用域中,类名即可指代当前类,又可指代当前模板名称,需要多加分辨。
2.3. 注入类名与构造函数
在类作用域中,注入类名被当作构造函数的名称,由此引入了一个需要注意的规则:
在限定名C::D解析过程中,如果D是C作用域中的注入类名,且编译器认为C::D可能是一个函数,那么该限定名一定会被解析成构造函数:
不过事实上,只有当D和C同名时,C才会是D作用域的注入类名,毕竟对于D作用域而言,唯一的注入类名就是D了。。也就是说,C::D规则其实就是C::C规则,当然,标准里面的表述方式逻辑上也没毛病,就是理解起来差点意思。
1 | struct A { |
函数相关
1. ADL - Argument-dependent lookup
https://en.cppreference.com/w/cpp/language/adl
在编写函数调用(包括操作符函数)语句时,如果函数没有限定符并且在当前环境下找不到定义,编译器根据函数参数的限定符去推测函数限定符的行为。
异常相关
1. Function-try-block
https://en.cppreference.com/w/cpp/language/function-try-block
基础概念
1. ODR - One Definition Rule
https://en.cppreference.com/w/cpp/language/definition#One_Definition_Rule
一个符号可以被多次声明,但只能定义一次。
2. ill-formed
非良构。当文档中提及ill-formed时,指的是一个遵从标准的C++编译器应该识别这种情况并给出明显提示。
名字查找
为了编译std::cout << std::endl,编译器进行了:
- 名字
std的无限定的名字查找,找到了头文件<iostream>中的命名空间 std 的声明 - 名字
cout的有限定的名字查找,找到了命名空间std中的一个变量声明 - 名字
endl的有限定的名字查找,找到了命名空间std中的一个函数模板声明 - 名字
operator <<的两个实参依赖查找找到命名空间std中的多个函数模板声明,而名字std::ostream::operator<<的有限定名字查找找到声明于类std::ostream中的多个成员函数
对于函数和函数模板的名字,名字查找可以将同一个名字和多个声明联系起来,而且可能从实参依赖查找中得到额外的声明。还会进行模板实参推导,并将声明的集合交给重载决议,由它选择所要使用的那个声明。如果适用的话,成员访问的规则只会在名字查找和重载解析之后才被考虑。
1. 有限定的名字查找
限定名,是出现在作用域解析操作符 **::** 右边的名字(参阅有限定的标识符)。 限定名可能代表的是:
- 类的成员(包括静态和非静态函数、类型和模板等)
- 命名空间的成员(包括其它的命名空间)
- 枚举项
若 **::** 左边为空,则查找过程仅会考虑全局命名空间作用域中作出(或通过 using 声明引入到全局命名空间中)的声明。这样一来,即使局部声明隐藏了该名字,也能够访问它。
在能对 **::** 右边的名字进行名字查找之前,必须完成对其左边的名字的查找(除非左边所用的是 decltype 表达式或左边为空)。对左边的名字所进行的查找,根据这个名字左边是否有另一个 **::** 可以是有限定或无限定的,但其仅考虑命名空间、类类型、枚举和能特化为类型的模板(这一句话的意思参考下面的例子)。
1 | struct A { |
若 **::** 后跟字符 **~** 再跟着一个标识符(也就是说指定了析构函数或伪析构函数),那么该标识符将在 **::** 左边的名字相同的作用域中查找。下面的例子可以让你喝一壶:
1 | struct C { typedef int I; }; |
1.1.1. 枚举项
若对左边的名字的查找结果是枚举(无论是有作用域还是无作用域),右边名字的查找结果必须是属于该枚举的一个枚举项,否则程序非良构。
1.1.2.
1.1.3. 类成员
若对左边的名字的查找结果是某个类、结构体或联合体的名字,则 **::** 右边的名字在该类、结构体或联合体的作用域中进行查找(因此可能找到该类或其基类的成员的声明),但有以下例外情况:
- 析构函数按如上所述进行查找(即在 :: 左边的名字的作用域中查找)
- 用户定义转换函数名中的转换类型标识( conversion-type-id ),首先在该类类型的作用域中查找。若未找到,则在当前作用域中查找该名字。
- 模板实参中使用的名字,在当前作用域中查找(而非在模板名的作用域中查找)
- using 声明中的名字,还考虑在当前作用域中声明的变量、数据成员、函数或枚举项所隐藏的类或枚举名
若 **::** 右边所指名的是和其左边相同的类,则右边的名字表示的是该类的构造函数。这种限定名仅能用在构造函数的声明以及引入继承构造函数的 using 声明中。在所有忽略函数名的查找过程中(即在查找 **::** 左边的名字,或查找详述类型说明符或基类说明符中的名字时),则将同样的语法解释成注入类名( injected-class-name ):struct A::A a2; a2类型就是struct A。
有限定名字查找可用来访问被嵌套声明或被派生类隐藏了的类成员。对有限定的成员函数的调用将不再是虚调用。
1.1.4. 命名空间的成员
若 **::** 左边的名字代表的是命名空间,或者 **::** 左边为空(这种情况其代表全局命名空间),那么 **::** 右边的名字就在这个命名空间的作用域中进行查找,但有以下例外:
- 在模板实参中使用的名字在当前作用域中查找
1 | namespace N { |
在命名空间 N 中进行有限定查找时,首先要考虑处于 N 之中的所有声明,以及处于 N 的内联命名空间成员(并且传递性地包括它们的内联命名空间成员)之中的所有声明。如果这个集合中没有找到任何声明,则再考虑在 N 和 N 的所有传递性的内联命名空间成员中发现的所有using 指令所指名的命名空间之中的声明。这条规则是递归实施的:
1 | int x; |
上面的例子中多次定义同一个符号是违法的,但是同一个声明允许被多次找到:
1 | namespace A { int a; } |
2. 无限定的名字查找
3. 最内层的外围命名空间
这是英文原文:innermost enclosing namespace 的标准中文表述。
这种表述出现在对友元引入的名字的查找中:
若所查找的是由友元声明所引入的名字:这种情况下仅考虑其最内层的外围命名空间,否则的话,对外围命名空间的查找将照常持续直到全局作用域。
指的是,如果通过friend引入了名字A,当使用A::a时,只会在A所限定的名字空间中查找a,该查找过程不会扩展到A所处的名字空间,这样的一个严格限定的名字空间就叫做a的最内层的外围名字空间(innermost enclosing namespace)。