10180 字
51 分钟
C++杂记 - 5(三):名称查找、翻译单元、std::string 实现与 TCMalloc
2023-03-07

标识符, 限定标识符,有限定名称查找,无限定名称查找和ADL 实参依赖查找#

标识符#

C++编译器将文件代码源文件解析后,将代码分解为identifier1、数值、运算符等,其中identifier是由非数字开头、任意字符数字和下划线组成的部分,其用来组成声明、表达式、name和qualified identifier。

在声明中identifier:

  • 不能时语法关键字
  • 不要以双下划线(__)或者下划线(_)开头,以免和编译器或者标准库的内部声明冲突,可以参见17.6.4.3 [reserved.names]

identifier在表达式中除了表示一些简单的函数和对象外,还可以是:

  • 函数写法的重载运算符名。比如 operator+ or operator new;
  • the name of an operator function, such as operator+ or operator new;
  • 用户定义转换函数的名字,比如 operator bool;
  • 用户定义字面量运算符的名字 比如 operator "" _km;
  • ~字符后随类名,如~MyClass;
  • ~ 字符后随decltype说明符,比如~decltype(str);
  • 模板名后随包含模板实参的角括号 比如MyTemplate<int>;
  • 作用域解析操作符::限定的标识符 例如 std::string or ::tolower.

限定标识符#

qualified identifier(限定标识符)是由域解析符::标识与class名、枚举类名、namespace或者decltype表达式限定的一类identifier。

比如:

  • std::string::npos
  • ::tolower
  • ::std::cout
  • boost::signals2::connection

名称#

name是指下面的一个实体或标签:

当编译器遇到一个未知的name时,会进行name lookup2,例如,当编译std::cout << std::endl;时:

  • 因为std左侧没有::进行限定,则对std进行unqualified name lookup,发现其是一个声明在头文件<iostream>中的namespace
  • 因为cout左侧有::进行限定且其限定的域为一个namespace,则对cout进行qualified name lookup,发现其是一个声明在namespace std中的变量
  • 因为endl左侧有::进行限定且其限定的域为一个namespace,则对endl进行qualified name lookup,发现其是一个声明在namespace std中的函数模板
  • <<没有限定且为一个函数,对<<进行argument-dependent lookup,发现其是一个声明在namespace std中的函数模板声明

其主要规则是,如果目标是一个qualified identifier(限定标识符),进行Qualified name lookup3,否则进行Unqualified name lookup4,对于函数还可能进行Argument-dependent lookup5

有限定名称查找 Qualified name lookup#

当遇到未知的qualified identifier(限定标识符)时,会去其对应的限定符区域内进行查找,比如命名空间、类作用域、枚举空间等。

std::cout << 1; // 解析"cout"时,去命名空间std中进行查找
struct A {
typedef int type;
};
A::type a; // 解析"type"时,去A类作用域中查找

如果::左侧没有限定符,则去全局命名空间进行查找,这样就避免了被本地声明遮盖的情况:

#include <iostream>
int main() {
struct std {};
std::cout << "fail\n"; // Error: unqualified lookup for 'std' finds the struct
::std::cout << "ok\n"; // OK: ::std finds the namespace std
}

当然,在解析::右手侧的name之前需要先解析::左手侧的name(除非使用了decltype表达式或者::左侧为空)。至于带::的name查找时qualified name lookup还是unqualified name lookup,取决于::左侧的name。当::左侧name为namespace、class、枚举、实例化的模板时,为qualified name lookup。

struct A {
static int n;
};
int A::n = 0;
int main() {
int A;
A::n = 42; // OK: 对"A"进行"unqualified lookup"时,忽略了本地变量A
A b; // 错误: 对"A"进行"unqualified lookup"会指向本地变量A
struct A b; // A 前面需要增加tag "struct" 进行限定,以避免被本地变量A遮盖
}

qualified name用于声明时,当同在一个声明中的unqualified name需要进行unqualified lookup时,在qualified name对应的类作用域、命名空间进行查找。不在同一个声明中的unqualified name,不会受到这个影响。同时也不受声明类型的影响。

上面这一段话比较难描述,需要结合一个例子来理解:

class X {};
constexpr int number = 100;
struct C {
class X {};
static const int number = 50;
static X arr[number];
};
X C::arr[number],
brr[number]; // 错误: "X"经过`unqualified name`解析为`::X`与`C::arr`对应的`C::X`不同。
C::X C::arr[number], brr[number]; // OK: `C::arr`的长度为50,`brr`的长度为100
// `C::arr[number]`中的`number`受同一个声明中的"qualified name"`C::arr`
// 的影响,回去类作用域`C`中查找,得到`C::number`,也就是50。而`brr[number]`
// 与`C::arr[number]`不是同一个声明,则不受这种影响,得到`::number`,也就是100

如果 :: 后随字符 ~ 再跟着一个标识符(也就是说指定了析构函数或伪析构函数),那么该标识符将在与 :: 左边的名字相同的作用域中查找。

struct C {
typedef int I;
};
typedef int I1, I2;
extern int *p, *q;
struct A {
~A();
};
typedef A AB;
int main() {
p->C::I::~I(); // 解析`~I()`时,使用`C::I`这个`name`相同的域`C::`,则找到了`C::I`
q->I1::~I2(); // 解析`~I2()`时,使用`::I1`这个`name`相同的域,则找到了`::I2`
AB x;
x.AB::~AB(); // ~ 之后的名字 AB 在 :: 前面的 AB 的同一个作用域中查找
// 也就是说从当前的作用域中查找,因此查找结果是 ::AB
}

当::左侧为枚举类时,则::右侧的name必须属于这个枚举类,否则为ill-formed

qualified name lookup还可以被用来调用被隐藏的方法,这种调用方式不会调用虚函数:

struct B {
virtual void foo();
};
struct D : B {
void foo() override;
};
int main() {
D x;
B &b = x;
b.foo(); // calls D::foo (virtual dispatch)
b.B::foo(); // calls B::foo (static dispatch)
}

模板参数进行解析时,从当前域进行解析

namespace N {
template <typename T>
struct foo {};
struct X {};
} // namespace N
N::foo<X> x; // 错误:"X"被解析为"::X",而不是"N::X"

对一个namespace N进行qualified name lookup时,首先考虑namespace N内的声明和inline namespace members。如果没有匹配的,其次考虑using-directives导入到namespace N中的声明

int x;
namespace Y {
void f(float);
void h(int);
} // namespace Y
namespace Z {
void h(double);
} // namespace Z
namespace A {
using namespace Y;
void f(int);
void g(int);
int i;
} // namespace A
namespace B {
using namespace Z;
void f(char);
int i;
} // namespace B
namespace AB {
using namespace A;
using namespace B;
void g();
} // namespace AB
void h() {
AB::g(); // 首先考虑"AB::g",则不再对namespace A、B进行查找
AB::f(1); // 首先在namespace AB中查找,未匹配,然后到namespace A、B中查找,查找到"A::f"和"B::f",
// 有匹配,则不再对namespace Y进行查找。然后在"A::f"和"B::f"中间选择了"A::f(int)"。
AB::x++; // 首先在namespace AB中查找,未匹配。然后到namespace A、B中查找,未匹配。
// 然后到namespace Y、Z中查找,未匹配。然后报错。
AB::i++; // 首先在namespace AB中查找,未匹配。然后到namespace A、B中查找,匹配到"A::i"和"B::i"
// 存在冲突,报错。
AB::h(16.8); // 首先在namespace AB中查找,未匹配。然后到namespace A、B中查找,未匹配。
// 然后到namespace Y、Z中查找,匹配到"Y::h"和"Z::h",选择"Z::h(double)"
}

在namespace中进行qualified name lookup时,允许通过不同途径匹配到相同的类型

namespace A {
int a;
} // namespace A
namespace B {
using namespace A;
} // namespace B
namespace D {
using A::a;
} // namespace D
namespace BD {
using namespace B;
using namespace D;
} // namespace BD
void g() {
BD::a++; // 首先在 namespace BD 中查找,未匹配。然后到 namespace B、D中查找,未匹配。
// 再下面,namespace B、D都同时指向了 namespace A,则在 namespace A 中匹
// 配到 A::a,选择 "A::a"
}

无限定名称查找#

unqualified name只要指那些左侧没有::域符号限定的name。在搜索时,在相关命名空间、using引入的命名空间等域进行,直到找到一个匹配的类型则停止。

当遇到一个未知的unqualified name时,会从当前文件域、命名空间开始查找,具体情形比较简单。下面列出一些需要特殊注意的点:

一个变量的定义在其命名空间X外时且定义语句中引用了unqualified name,解析这个unqualified name时,首先从变量命名空间X开始查找。有点类似ADL,但是注意区别。

namespace X {
extern int x; // 声明,不是定义
int n = 1; // 在解析x的定义时,其引用了unqualified name “n”,先从其定义域"X"开始搜索,首先发现 n=1
}; // namespace X
int n = 2; // 如果X::n不存在时,则匹配::n,否则被遮盖。
int X::x = n; // X::x的定义式,X::x的定义在其命名空间"X"外,且引用了unqualified name “n”,
// 在解析"n"时,匹配X::n,则X::x为1

当在命名空间外定义一个非成员函数时,如果函数内引用了unqualified name时,解析这个unqualified name时,依次搜索其函数定义之前的:本地代码块、代码块外层定义变量、函数定义之前的命名空间、函数定义之前的外层命名空间、函数定义之前的全局域,直到找到一个匹配的name,在函数定义之后的相关域不在搜索范围内。

namespace A {
namespace N {
void f();
int i = 3; // 3: 查找"命名空间",如果将①②注释掉,这句定义存在,则"i"的定义匹配此处,则i=3
} // namespace N
int i = 4; // 4: 查找"外层命名空间",如果将①②③注释掉,这句定义存在,则"i"的定义匹配此处,则i=4
} // namespace A
int i = 5; // 5: 查找"全局域",如果将①②③④注释掉,这句定义存在,则"i"的定义匹配此处,则i=5
// 在命名空间外定义了一个非成员函数f(),这个例子的注释需要按照序号来看。
void A::N::f() {
int i = 2; // 2: 其次查找"外层代码块",如果将①注释掉,这句定义存在,则"i"的定义匹配此处,则i=2
{
int i = 1; // 1: 首先查找“本地代码块”,如果这句定义存在,则"i"的定义匹配此处,则i=1
std::cout << i; // 0: 在代码块中引用了unqualified name "i",则需要查找"i"的定义
}
}
// int i; // 如果将5移动到f()定义之后则不参与"i"的查找
namespace A {
namespace N {
// int i; // 如果将3移动到f()定义之后则不参与"i"的查找
}
} // namespace A

对于在类的定义中所使用的unqualified name,当出现于除了在成员函数体、成员函数的默认实参、成员函数的异常规定、默认成员初始化器、契约条件 (C++20 起)或者嵌套类的定义(包括嵌套类从之派生的基类的名字)以外的任何位置时,要在下列作用域中查找:

  • 类体之中直到这次使用点之前的部分以及其基类的整个类体
  • 基类的整个类体,找不到声明时,递归到基类的基类中
  • 当这个类是嵌套类时,其外围类体中直到这个类的声明之前的部分以及外围类的基类的整个类体
  • 当这个类是局部类或局部类的嵌套类时,定义了这个类的块作用域中直到其定义点之前的部分
  • 当这个类是命名空间的成员,或者命名空间成员类的嵌套类,或者命名空间成员函数的局部类时,查找这个命名空间作用域中直到这个类、其外围类或函数的定义之前的部分。若所查找的是由友元声明所引入的名字:这种情况下仅考虑其最内层的外围命名空间,否则的话,对外围命名空间的查找将照常持续直到全局作用域。
namespace M {
// const int i = 1; // 找不到这个
class B {
// const const int i = 3; // 找到了第三个(但之后会被访问检查所拒绝)
};
} // namespace M
// const int i = 5; // 找到了第五个
namespace N {
// const int i = 4; // 找到了第四个
class Y : public M::B {
// static const int i = 2; // 找到了第二个
class X {
// static const int i = 1; // 找到了第一个
int a[i]; // use of i
// static const int i = 1; // 找不到这个
};
// static const int i = 2; // 找不到这个
};
// const int i = 4; // 找不到这个
} // namespace N
// const int i = 5; // 找不到这个

当在类作用域外定义一个成员函数时,如果函数内引用了unqualified name时,解析这个unqualified name时,依次搜索其函数定义之前的:本地代码块、代码块外层定义变量、类作用域、基类作用域、函数定义之前的命名空间、函数定义之前的外层命名空间、函数定义之前的全局域,直到找到一个匹配的name,在函数定义之后的相关域不在搜索范围内。注意跟上面非成员函数的区别。

class B {
int i; // 4: 如果1 2 3不存在,”i“匹配此处
};
namespace M {
int i; // 6: 如果1 2 3 4 5不存在,”i“匹配此处
namespace N {
int i; // 5: 如果1 2 3 4不存在,”i“匹配此处
class X : public B {
int i; // 3: 如果1 2不存在,”i“匹配此处
void f(); // 0: class X中声明了一个函数f()
// int i; // 将3移动到这里也OK,类成员的可见性受声明的位置的影响。
};
// int i; // 5移动到这里也可以,跟f()实现代码的位置有关,与f()声明的位置无关
} // namespace N
} // namespace M
int i; // 7: 如果1 2 3 4 5 6 7不存在,”i“匹配此处
// 定义class X中声明的函数f()
void M::N::X::f() {
int i; // 2: 如果2不存在,”i“匹配此处
{
int i; // 2: 如果2存在,”i“首先匹配此处
i = 16;
}
// int i; // 如果这句存在,1 2都不存在,也不会匹配此处,因为其出现在调用i的代码块之后
}
namespace M {
namespace N {
// int i; // 5移动到这里不匹配,其出现在f()实现代码之后
}
} // namespace M

虚继承的优先性,这个概念用定义来讲非常麻烦,简单来讲就是:如果一个类A由类B派生而来、类B虚继承自类C,则在类A中进行非限定查找时,类B中的符号会隐藏遮盖掉类C中的name,如果类A还通过其他非虚继承的方式继承了类C,则类B不会隐蔽遮盖类C中的相同name,发生歧义错误。

struct X {
void f();
};
struct B1 : virtual X {
void f();
};
struct B2 : virtual X {};
struct D : B1, B2 {
void foo() {
X::f(); // OK,调用了 X::f(有限定查找)
f(); // OK,调用了 B1::f(无限定查找)
/*
C++98 规则:B1::f 隐藏 X::f,因此即便从 D 通过 B2 可以访问到 X::f,它也不能从 D 中的名字查找所找到。
C++11 规则:在 D 中对 f 的查找集合并未找到任何东西,继续处理其基类。 在 B1 中对 f 的查找集合找到了 B1::f,并且完成查找合并时替换了空集,此时在 C 中 对 f 的查找集合包含 B1 中的 B1::f。在 B2 中对 f 的查找集合并未找到任何东西,继续处理其基类。在 X 中对 f 的查找找到了 X::f合并时替换了空集,此时在 B2 中对 f 的查找集合包含 X 中的 X::f。当向 C 中合并时发现在 B2 的查找集合中的每个子对象(X)都是已经合并的各个子对象(B1)的基类,因此 B2 的集合被丢弃。C 剩下来的就是在 B1 中所找到的 B1::f (如果使用 struct D : B2, B1,则最后的合并将会替换掉。C 此时已经合并的 X 中的 X::f,因为已经加入到 C 中的每个子对象(就是 X)都是新集合(B1)中的至少一个子对象的基类,其最终结果是一样的:C 的查找集合只包含在 B1 中找到的 B1::f)
*/
}
};
#include <iostream>
struct X {
void f() { std::cout << "X::f()" << std::endl; }
};
struct B1 : virtual X {
void f() { std::cout << "B1::f()" << std::endl; }
};
struct B2 : X {};
struct D : B1, B2 {
void foo() {
X::f(); // OK,调用了 X::f(有限定查找)
f(); // 由于D还通过B2的渠道非虚继承了X,则X::f()不会被B1::f()遮盖掉,所以此处发生歧义错误。
};
};
struct V {
int v;
};
struct A {
int a;
static int s;
enum { e };
};
struct B : A, virtual V {};
struct C : A, virtual V {};
struct D : B, C {};
void f(D &pd) {
++pd.v; // OK:只有一个 v,因为只有一个虚基类子对象
++pd.s; // OK:只有一个静态的 A::s,即便在 B 和 C 中都找到了它
int i = pd.e; // OK:只有一个枚举符 A::e,即便在 B 和 C 中都找到了它
++pd.a; // 错误,有歧义:B 中的 A::a 和 C 中的 A::a
}

类的静态成员定义时,如果引用了unqualified name,其查找过程与类成员函数中unqualified name的查找顺序相同

struct X {
static int x;
static const int n = 1;
};
int n = 2;
int X::x = n; // 找到了 X::n,将 X::x 设置为 1 而不是 2

当一个类的友元函数被定义在类作用域内部时,该友元函数中引用的unqualified name的查找顺序与该类的成员函数中的unqualified name的查找顺序相同。当类的友元函数被定义在类作用域外部时,该友元函数中引用的unqualified name的查找顺序与其所在命名空间的其他函数中的unqualified name的查找顺序相同。

int i = 3; // 3: 当1 2不存在时,f1中的"i"匹配此处
// 3: 当1 2不存在时,f2中的"i"也不会匹配此处
struct X {
static const int i = 2; // 2: 当这句定义存在时,1不存在时,f1中的"i"匹配此处
// 2: 当1不存在时,f2中的"i"也不会匹配此处
friend void f1(int x)
{
int i; // 1: 当这句定义存在时,f1中的"i"匹配此处
i = x; // 0: 友元函数f1(int)被定义在类X作用域内部时,"i"的查找顺序与X的成员函数查找顺序相同
}
friend int f2();
// static const int i = 2; // ②移动到此处也OK
};
void f2(int x) {
int i; // 1: 当这句定义存在时,f2中的"i"匹配此处
i = x; // 0: 友元函数f2(int)被定义在类X作用域外部时,"i"的查找顺序f2(int)所在命名空间的普通函数的查找顺序相同
}

当一个类A的成员函数被声明为类B的友元,且该声明中包含unqualified name,则对unqualified name进行查找时:

  • 如果unqualified name不是任何模板的参数,则首先去类作用域A中进行查找
  • 如果在类作用域A中未匹配或者其实模板参数,则去类作用域B中进行查找
// 这个类的成员函数被作为友元
struct A {
typedef int AT;
void f1(AT);
void f2(float);
template <class T>
void f3();
};
// 这个类授予友元关系
struct B {
typedef char AT;
typedef float BT;
friend void A::f1(AT); // 对 "AT" 的查找时,先到类作用域A中进行查找,匹配"A::AT"
friend void A::f2(BT); // 对 "BT" 的查找时,先到类作用域A中进行查找,未匹配,
// 再去类作用域B中查找,匹配"B::BT"
friend void
A::f3<AT>(); // 对 "AT" 的查找时,"AT"是模板参数,则直接去类作用域B中查找,匹配"B::AT"
};

unqualified name被当做函数默认参数时,查找其定义时,首先考虑同一函数声明中的形参:

class X {
int a, b, i, j;
public:
const int &r;
X(int i)
: r(a), // 将 X::r 初始化为指代 X::a
b(i), // 将 X::b 初始化为形参 i 的值
i(i), // 将 X::i 初始化为形参 i 的值
j(this->i) // 将 X::j 初始化为 X::i 的值
{}
}
int a;
int f(int a, int b = a); // 错误:对 a 的查找找到了形参 a,而不是 ::a
// 但在默认实参中不允许使用形参

在枚举类定义时,unqualified name的查找首先考虑当前枚举类的作用域

const int RED = 7;
enum class color {
RED,
GREEN = RED + 2, // RED 找到了 color::RED ,而不是 ::RED ,因此 GREEN = 2
BLUE = ::RED + 4 // 通过 qualified name lookup 找到 ::RED , BLUE = 11
};

在”try-catch”语句中,unqualified name的查找跟函数体内引用的unqualified name查找一样。平行的代码块内的定义不可见。

int n = 3; // 3
int f(int n = 2) { // 2
try {
int n = -1; // 不会匹配到该处
} catch (...) {
// int n = 1; // 1: 加入此处存在,则匹配该处
assert(n == 2); // 0: n 按1 2 3的顺序进行依次查找,匹配f的参数n,即2
throw;
}
return 0;
}

对于在表达式中所使用的重载运算符(比如在 a+b 中使用的 operator+),其查找规则和对在如operator+(a,b)这样的显式函数调用表达式中所使用的运算符是有所不同的:当处理表达式时要分别进行两次查找:对非成员的运算符重载,也对成员运算符重载(对于同时允许两种形式的运算符)。然后将这两个集合和在重载解析所述内建的运算符重载以平等的方式合并到一起。而当使用显式函数调用语法时(如operator+(a,b)),则进行常规的unqualified name lookup:

struct A {};
void operator+(A, A); // 用户定义的非成员 operator+
struct B {
void operator+(B); // 用户定义的成员 operator+
void f();
};
A a;
void B::f() // B 的成员函数定义
{
operator+(a, a); // 错误:在成员函数中的常规名字查找
// 找到了 B 的作用域中的 operator+ 的声明
// 并于此停下,而不会达到全局作用域
a + a; // OK:成员查找找到了 B::operator+,非成员查找
// 找到了 ::operator+(A,A),重载决议选中了 ::operator+(A,A)
}

注入类名#

在一个类作用域中,当前类的名称被当做公开的成员名一样对待 也就是说一个类的名字会以unqualified-name的形式,被注入到该类的作用域内。

因此我们在类(类模板)的作用域内可以通过unqualified-name的形式来指代该类。然而,qualified-name的形式则不可以用来指代该类,因为这种形式被用来指代构造函数。

int X;
struct X {
void f() {
X* p; // OK : X
// 指代注入类名,在"X"的作用域中,"X"被当做公开成员,注意体会这个"公开成员的含义"
::X* q; // 错误:在全局作用域"::"中,"struct X"被"int X"遮盖
}
};
//-------------------------另外一个例子---------------------
int32_t C;
class C {
private:
int64_t i;
public:
static size_t f() {
static_assert(sizeof(C) == sizeof(int64_t));
return sizeof(C); // C is injeceted as an `unqualified-name`
// into the class scope
}
static void g() {
auto p = &::C; // '::C' refers to the global variable if it
// is qualified with '::'
static_assert(std::is_same_v<decltype(p), int32_t*>);
}
static void k() {
// auto p = &C::C;
// error: taking address of constructor 'constexpr C::C(C&&)'
}
};
auto f() -> size_t {
static_assert(sizeof(C) == sizeof(int32_t));
return sizeof(C); // f in not in the C class scope, thus 'C'
// refers to the global variable 'int32_t C'
}

在类继承的过程中,受到继承限制的控制

namespace detail {
struct A {};
struct B {};
}; // namespace detail
struct C : public detail::B, private detail::A {};
struct D : public C {
A* a0; // 错误:注入类名 A 受到“private”的修饰,变为非公开成员,不可访问
detail::A* a1; // OK:不使用注入类名
B* b0; // OK:通过注入类名
detail::B* b1; // OK:不使用注入类名
};

在类模板中,类似普通类情形一样,可以被注入,当做模板名或者类型名。有下列3个情形之一时,注入的名称被当做当前模板名:

  • 它后面紧跟随 < 符号(模板实例化标识)
  • 它被当做一个模板模板参数(template template parameter)
  • 它是友元类模板声明的详细类型指定符中的最后标识符。 此外其他情形,会被当做一个实际类类型被注入,其类型为该类模板实例化后的类型。
// 注意体会这个例子
template <template <class, class> class>
struct A;
template <class T1, class T2>
struct X {
X<T1, T2>* p; // X后跟随"<",则X被当做模板 template<class, class> struct X 对待
// 同理,此处改为 X<int, int> *p 也是成立的,因为X是一个模板
using a =
A<X>; // X被当做模板A的模板模板参数,则X被当做模板 template<class, class> struct X 对待
template <class U1, class U2>
friend class
X; // X被当友元模板类的标识符,则X被当做模板 template<class, class> struct X 对待,即::X
// 此处含义为,当模板X实例化后,X<T1,T2>拥有友元X<U1,U2>,X<U1,U2>可以被实例化为多个类型
X* q; // 此处为以上三种情形之外,X被当做了一个实例化的类型名,其类型为X<T1,T2>。
// 当X<T1,T2>被实例为X<int,int>时,q的类型为`X<int,int> *`
// 当X<T1,T2>被实例为X<double,double>时,q的类型为`X<double,double> *`
};

ADL实参依赖查找#

ADL主要适用于对unqualified-name的函数进行的名字查找。查找对象是一个在进行函数调用或者运算符调用的非成员函数。换句话说,ADL依据函数调用中的实参的数据类型查找未限定(unqualified)的函数名(或者函数模板名)。

哪些不触发ADL#

如果通常的未限定(unqualified)名字查找所产生的候选集包括下述情形,则不会启动依赖于实参的名字查找(ADL查找不会进行):

1.类成员声明(此种情形仅指普通的类成员函数,不指类成员运算符函数)

#include <iostream>
namespace xx {
struct XA {};
void foo(XA &a) { std::cout << "xx::foo" << std::endl; }
}; // namespace xx
namespace yy {
void test() {
xx::XA xa;
foo(xa); // 在这里,可以通过ADL来找到xx::foo
}
struct YA {
int foo;
static void test() {
xx::XA xa;
foo(xa); // 在这里,由于通过unqualified name lookup找到了
// 成员变量int foo,所以不会进行ADL,因此会出错
}
};
} // namespace yy

2.块作用域内的函数的声明,不含(using-declaration)

#include <iostream>
namespace xx {
struct XA {};
void foo(XA &a) { std::cout << "xx::foo" << std::endl; }
}; // namespace xx
namespace zz {
void foo(int a) { std::cout << "zz::foo" << std::endl; }
} // namespace zz
namespace yy {
void foo(int a); // 1
void test0() {
xx::XA xa;
foo(xa); // 此处正常进行ADL,不受到外部声明1的影响 因为1不在块作用域内
}
void test1() {
using zz::foo; // 2
xx::XA xa;
foo(xa); // 此处正常进行ADL,不受声明2的影响 因为2使用了using declaration
}
void test2() {
void foo(int); // 3
xx::XA xa;
foo(xa); // ERROR 此处由于该块作用域内中有不含using的声明,则不会进行ADL,所以会报错
}
} // namespace yy

3.任何不是函数或者函数模板的声明(例如函数对象或者另一个变量其名字与被查询的函数名字冲突)

注意,这个是CPO的要点。

namespace xx {
struct XA {};
void foo(XA &a) { std::cout << "xx::foo" << std::endl; }
}; // namespace xx
namespace zz {
struct foo_callable {
void operator()(xx::XA) const {
std::cout << "zz::foo functor called for xx::XA" << std::endl;
}
};
foo_callable foo{}; // 全局可用的CPO实例
}
namespace yy {
void foo(int a);
void test0() {
xx::XA xa;
foo(xa); //  一切正常
}
void test1() {
using zz::foo; // 由于该处声明,引入的是一个函数对象,则1处不会进行ADL,而是直接调用zz::foo
// 去掉这个这正常调用xx::foo
xx::XA xa;
foo(xa); // 1
}
} // namespace yy
int main() {
yy::test1();
return 0;
}

4.当调用的函数被括号包围时

#include <iostream>
namespace xx {
struct XA {};
void foo(XA &a) { std::cout << "xx::foo" << std::endl; }
}; // namespace xx
namespace yy {
void test0() {
xx::XA xa;
foo(xa); // OK,此处进行ADL
(foo)(xa); // ERROR,由于foo被括号包围,故不进行ADL,这条规则在文档中没有提及,
// 不知道应该被归属于哪一条,所以单独拿出来。
}
} // namespace yy

ADL的查找范围#

函数调用表达式的每个实参的类型用于确定命名空间与类的相关集合(associated set of namespaces and classes)并用于函数名字查找(这句话的意思简而言之就是ADL查找的集合范围如何确定):

1.基本类型(fundamental type)实参的命名空间与类的相关集合为空。

这个的意思是,当参数的类型是基本类型时(例如 int, char, double 等),ADL 的处理方式如下:

  1. 基本类型的命名空间和类集合为空:
    • 当 ADL 处理一个基本类型(如 int)的参数时,它不会将任何命名空间或类添加到与该类型相关联的查找集合中。基本类型不属于任何用户定义的命名空间或类,因此它们的关联命名空间和类集合为空。
  2. ADL 对基本类型不起作用:
  • 由于基本类型没有相关的命名空间或类,因此 ADL 不会基于这些类型查找到任何附加的函数定义。换句话说,当函数参数是基本类型时,ADL 不会帮助找到在某个特定命名空间中定义的函数。
如果参数的命名空间为空,将其添加到查找集合内,但是查找时,会直接跳过空集。

2.类类型(class type,指struct,class,union类型),相关集合包括

  1. 类类型自身;
  2. 该类型的所有的直接或间接基类;
  3. 如果类类型 T 是另一个类 G 的成员(嵌套类型),则那个包含了类类型 T 的类 G;
  4. 该类类型的所有相关类的最内层外围命名空间。
#include <iostream>
namespace ADL {
struct A;
struct Base {
friend void func2(const A &a) { std::cout << "2.2" << std::endl; }
};
struct A : public Base {
friend void func1(A a) { std::cout << "2.1" << std::endl; }
struct B {};
friend void func3(B b) { std::cout << "2.3" << std::endl; }
};
void func4(A a) { std::cout << "2.4" << std::endl; }
}; // namespace ADL
int main() {
ADL::A a;
ADL::A::B b;
func1(a); // 2.1
func2(a); // 2.2
func3(b); // 2.3
func4(a); // 2.4
}

3.如果实参是类模板特化后得到的类型,在上述规则外,还检验下列规则,并添加其关联类与命名空间到集合:

  1. 类型模板形参(type template parameter)所对应的模板实参的类型,不包括非类型的模板形参、模板模板形参;
  2. 模板模板实参(template template argument)所在的命名空间;
  3. 模板模板实参所在的类(如果这个类包含了这个成员模板)。
  4. 对于枚举类型的实参,添加枚举类型所在的命名空间到集合。如果枚举类型是一个类的成员类型,则添加该类到集合。
#include <iostream>
namespace xx {
enum XType {
XTypeA,
XTypeB,
XTypeC,
};
void foo(XType x) { std::cout << "foo 4" << std::endl; }
struct A {
enum AType {
ATypeA,
ATypeB,
};
friend void foo1(AType x) { std::cout << "foo 41" << std::endl; }
};
} // namespace xx
namespace yy {
void test0() { foo(xx::XTypeA); } // ADL
void test1() { foo1(xx::A::ATypeA); } // ADL
} // namespace yy

5.如果实参是类型 T 的指针或者是类型 T 的数组的指针,则检验类型 T 并添加其类与命名空间的关联集到集合。

#include <iostream>
namespace xx {
struct A {
friend void foo1(A* x) { std::cout << "foo 6.1.1" << std::endl; }
friend void foo2(A x[]) { std::cout << "foo 6.1.2" << std::endl; }
};
void foo3(A* a) { std::cout << "foo 6.2.1" << std::endl; }
void foo4(A x[]) { std::cout << "foo 6.2.2" << std::endl; }
} // namespace xx
namespace yy {
void test0() {
xx::A* a;
xx::A aa[2];
foo1(a); // ADL
foo2(aa); // ADL
foo3(a); // ADL
foo4(aa); // ADL
}
} // namespace yy

6.如果实参是函数类型,那么检验函数参数类型与函数返回值类型,并添加其类与命名空间的关联集到集合。

#include <iostream>
namespace xx {
struct A {};
void func1(A& a) {}
A func2() { return {}; }
void lookup1(void (*f)(A&)) { std::cout << "xx:lookup1" << std::endl; }
void lookup2(A (*f)()) { std::cout << "xx:lookup2" << std::endl; }
} // namespace xx
namespace yy {
void test0() {
lookup1(xx::func1); // ok
lookup2(xx::func2); // ok
}
} // namespace yy

7.如果实参是类 X 的成员函数 F 的指针类型参数,那么该成员函数的形参类型、该成员函数返回值的类型、该成员函数所属类 X 的相关集合都被加入到关联集到集合。

#include <iostream>
namespace x {
struct B;
} // namespace x
namespace xx {
struct A {
x::B* f() { return nullptr; }
void ff(x::B&) {}
};
} // namespace xx
namespace x {
struct B {
friend void lookup1(B* (xx::A::*f)()) { std::cout << "xx::lookup1" << std::endl; }
};
template <typename T>
void lookup2(T t) {
std::cout << "xx::lookup2" << std::endl;
}
} // namespace x
namespace yy {
void test0() {
lookup1(&xx::A::f); // ok
lookup1(
nullptr); // failed,因为这条规则标明的是实参,此处实参是nullptr,则不会查找到对应的lookup1
lookup2(&xx::A::ff); // ok
}
} // namespace yy

8.如果实参是类 X 的数据成员 T 的指针类型参数,那么该成员类型、该数据成员所属类 X 的相关集合都被加入到关联集到集合。

#include <iostream>
namespace xx {
struct A {
int num = 0;
};
void lookup1(int A::*) { std::cout << "x::lookup1" << std::endl; }
} // namespace xx
namespace yy {
void test0() { lookup1(&xx::A::num); } // ok
} // namespace yy
int main() { yy::test0(); }

9.若参数是重载函数集的取址表达式(或对函数模板)的名称,则检验重载集中的每个元素,并添加其类与命名空间的关联集到集合。 1. 另外,若重载集为模板 id (带模板实参的模板名),则检验其所有类型模板实参与模板模板实参(但不含非类型模板实参),并添加其类与命名空间的关联集到集合。

#include <iostream>
namespace xx {
struct A {
int num = 0;
};
void lookup1(void (*)()) { std::cout << "x::lookup1" << std::endl; }
} // namespace xx
namespace yy {
void f() {}; // ①
void f(xx::A*) {}; // ②
void test0() { lookup1(&f); } // ok,由于重载f②的存在,namespace xx也被加入到ADL的集合中
} // namespace yy
int main() { yy::test0(); }

10.如果相关集合中的任何命名空间是内联命名空间(inline namespace), 则添加其外围命名空间到关联集合。

#include <iostream>
namespace adl {
inline namespace inner {
struct A {};
} // namespace inner
void func(const inner::A &a) { std::cout << "enclosed namespace added." << std::endl; }
} // namespace adl
int main() {
adl::inner::A a;
func(a); // 因为 adl::inner 为 inline namespace,则将其最内存外围 namespace adl
// 到查找关联集合中,则可以查找到 adl::func
}

11.如果相关集合中的一个命名空间直接包含了内联命名空间,则内联命名空间被增加到相关集合中。

#include <iostream>
namespace adl {
struct A {};
inline namespace inner {
void func(const A &a) { std::cout << "inline namespace added." << std::endl; }
} // namespace inner
} // namespace adl
int main() {
adl::A a;
func(a); // namespace adl中包含 inline namespace adl::inner,
// 则将 adl::inner 添加到查找关联集合中,则可以查找到 adl::inner::func
}

12.在确定命名空间与类的关联集后,为了进一步的 ADL 处理,忽略此集中所有于类中找到的声明,除了命名空间作用域的友元函数及函数模板,陈述于后述点2。以下列特殊规则,合并普通无限定查找找到的声明集合,与在 ADL 所生成关联集的所有元素中找到的声明集合:

  1. 忽略关联命名空间中的 using 指令
  2. 声明于关联类中的命名空间作用域友元函数(及函数模板)通过 ADL 可见,即使它们通过普通查找不可见。
  3. 忽略函数与函数模板外的所有名称(与变量不冲突)
#include <iostream>
namespace xx {
struct A;
}// namespace xx
namespace x {
void f(xx::A *) { std::cout << "x::f" << std::endl; }
} // namespace x
namespace xx {
using namespace x; // 1
struct A {};
} // namespace xx
namespace yy {
void test0() {
xx::A *a;
f(a); // not ok, ADL时忽略1处using指令
}
} // namespace yy
int main() { yy::test0(); }

通过 ADL 查找到的name会和前面unqualified name普通查找到的合并到一起进行选择,如果存在歧义,会报错。

ADL的核心是: 在调用函数的时候,只要有一个参数的类型属于函数所在的命名空间,那么调用的时候就不用加命名空间前缀

namespace A
{
struct Empty {};
void foo(int);
void bar(Empty, int);
}
void func()
{
A::foo(2); // 必须这么调用,foo(2)找不到A里面的foo
foo(A::Empty{}, 1); // 因为第一参数的类型在A中,foo前面不用再加前缀
std::cout << 1;
// 等价于operator<< (std::cout, 1)
// 由于ADL,可以找到std中的std::operator<< (std::ostream&, int)
}

CPO 定制点对象#

我们在ADL中的不触发ADL的第三点中提到了CPO的关键要素。CPO本质上,就是一个能够正确解决复杂定制问题的functor对象。为了解决这个问题,我们需要屏蔽掉ADL调用。也就是说,我们把ADL的**两步式(先using引入,再非限定调用)**放到了定制点对象内部进行。而在外部我们并不使用ADL。

  1. 它能够在编译期对于简单的无限定调用,根据实参的不同情况进行判断,并转发到正确的实现;
  2. 它能够检查实参与ADL重载(若存在)是否符合语法要求,并在需要的时候报错。
struct special{};
namespace mystd{
template<typename T>
void foo(T t) {
std::cout << "foo(T)" << std::endl;
}
}
namespace yb {
struct myclass {};
void foo(myclass s) { std::cout << "yb::foo" << std::endl; }
} // namespace yb
namespace xx {
struct XA {};
void foo(XA &a) { std::cout << "xx::foo" << std::endl; }
} // namespace xx
namespace yy {
void foo(int a) { std::cout << "yy::foo" << std::endl; }
void test0() {
xx::XA xa;
foo(xa); //  一切正常
}
namespace zz {
struct foo_callable {
template <typename... Args>
void operator()(Args &&...args) const {
// using xx::foo;
// using yb::foo;
// using yy::foo;
// 不用任何的using。因为我们传入的参数的namespace里都有一个叫foo的。所以正常ADL会找到的。
using mystd::foo; // 这个using是模拟swap例子里的using。
// 使用这个using的原因是mystd里面没有special这个成员。所以ADL也找不到。
std::cout << "zz::foo" << std::endl;
foo(std::forward<Args>(args)...); // 正常ADL
}
};
foo_callable foo{}; // 全局可用的CPO实例
} // namespace zz
void test1() {
using zz::foo; // 由于该处声明,引入的是一个函数对象,则1处不会进行ADL,而是直接调用zz::foo
xx::XA xa;
yb::myclass s;
foo(xa); // 1
foo(s); // 2
foo(10); // 3
foo(special{}); // 4
}
} // namespace yy
int main() {
yy::test1();
return 0;
}

参考资料

定制点对象

标识符、名字查找与实参依赖查找(ADL)

C++11中的name lookup

定制点对象和ADL

O’Dwyer的介绍

整理C++的三种名称查找方式#

查找类型触发形式搜索范围适用场景
限定名称查找 Qualifieda::b::x仅 :: 左侧指定作用域精确控制符号来源
非限定名称查找 Unqualifiedx由内向外逐层作用域局部变量、类成员、全局量
参数依赖查找 (ADL)func(arg)常规作用域 + 参数类型命名空间函数调用(尤其运算符)

来自参考

不要重新打开std空间并且往里塞东西#

比如如何正确的特化std::hash

不要这样

struct Widget {};
namespace std { // Danger!
template<>
struct hash<Widget> {
size_t operator()(const Widget&) const;
};
}

要这样

struct Widget {};
template<>
struct std::hash<Widget> {
size_t operator()(const Widget&) const;
};

参考这里

翻译单元#

在C++中,翻译单元指的是编译器处理的最小独立单元,通常对应一个源文件(.cpp或.c文件)以及其包含的所有头文件。具体来说,一个翻译单元包括以下内容:

  • 源代码文件自身的内容
  • 源代码文件中#include的所有头文件(Header Files)的内容,这些头文件在预处理阶段会被展开替换到源文件中
  • 宏定义(Macro Definitions)以及其他预处理器指令产生的文本替换

在编译过程中,每个源代码文件都会形成一个独立的翻译单元,编译器对每个翻译单元独立进行词法分析、语法分析、语义分析等编译步骤,最后生成对应的.o(对象文件)或.obj(在Windows系统中)。链接器随后会把这些独立的翻译单元链接起来,形成可执行文件或库文件。

std::string 的不同编译器实现和比较#

https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=109742Y3MOtw0Q:1716181180316&q=cpp+weekly&spell=1&sa=X&ved=2ahUKEwiLy_2MuZuGAxXha_UHHet8BmoQBSgAegQICBAB&biw=1920&bih=919&dpr=1

TCMalloc#

http://blog.gerryyang.com/linux%20performance/2022/08/07/tcmalloc-in-action.html

C++杂记 - 5(三):名称查找、翻译单元、std::string 实现与 TCMalloc
https://sterben-01.github.io/posts/杂记5-3/
作者
Sterben_01
发布于
2023-03-07
许可协议
CC BY-NC-SA 4.0