C++学习笔记--函数重载(1)
文章目录
- 序言
- 一、洞悉函数重载决议
- 1.1、重载决议的基本流程
- 1.2、Name Lookup
- 1.2.1、Qualified Name Lookup
- 1.2.1.1、Class Member Lookup
- 1.2.1.2、Namespace Member Lookup
- 1.2.2、Unqualified Name Lookup
- 1.2.2.1、Usual Unqualified Lookup
- 1.2.2.2、Argument Dependant Lookup
- 1.2.3、Template Name Lookup
- 1.2.4、Two-phase Name Lookup
序言
技术提高关键有三,一是输入,二是理解,三是输出。后二者皆受输入质量影响,低者产生浅理解弱输出,高者产生深理解强输出。是以书籍择取于学习者而言,至关重要。
限于所识,错误之处在所难免,敬请高明者匡正。
一、洞悉函数重载决议
大家可以尝试问自己一个问题:
调用一个重载函数,编译器是如何找到最佳匹配函数的?
若是你不能清楚地表述这个流程,就说明对函数重载决议缺乏认识。
函数重载决议也的确是许多 C++ 开发者都听过,但却从来没有真正理解过的一个概念,基本上也没有书籍深入讲解过这一重要概念,然而对语言的深刻理解往往就是建立在对这些基本概念的理解之上。
那么理解这个有什么用呢?
对于库作者,理解重载决议必不可少。因为重载涉及函数,函数又是变化的最小单元之一,可以说重载决议贯穿了「定制点」的发展历程。只有理解重载决议,才能理解各种定制点的表现方式,比如 ADL 二段式、CPOs、Deducing this。
对于使用者,若是不理解重载决议,就不能理解定制点,也就无法真正理解各种库的设计思路,使用起来难免会束手束脚。
同时,重载决议还涉及 ADL、TAD、SFINAE、Concepts、Forwarding reference 等等概念,若不理解重载决议,对这些相关概念的理解也将难以深入。
总而言之,重载决议与 C++ 中的许多概念都有着千丝万缕的联系,且重载解析本身就是一件非
常复杂的工作,这也是其难度颇高的原因。
接下来,就让我们拨开重重云雾,一探其究竟。
1.1、重载决议的基本流程
函数的标识主要分为两部分,名称和参数。
当函数名称唯一时,调用过程相对简单,直接查找即可。C 语言就属此列,它的函数名称必须唯一。
当函数名称相同,但参数类型不同时,在许多语言中依旧合法,此时这些名称相同的函数就称为重载函数。
C++ 就是支持重载函数的语言之一,那么它要如何来确定函数名的唯一性?
实际上,编译器会通过一种称为 Name mangling(名称修饰)的技术来为每个重载函数生成唯一的名称。虽然重载函数的名称是相同的,但其参数不同,因此通过名称 + 参数再辅以一些规则,生成唯一的名称其实并非难事。
但这仍非实现重载函数的关键与难点所在。名称是唯一产生了,但是用户并不知道,也并不能直接通过该名称来调用函数。用户调用的还是重载函数名称本身,此时就需要一套机制来解析实际调用的函数到底是哪个,该机制就是「重载决议」,由 C++ 标准制定。
简言之,只要遇到名称相同的函数,重载决议就会出现,用于找出最佳匹配函数。
那么问题又来了,它是如何知道存在哪些名称相同的函数?
这便是在重载决议出现之前的一项工作,称为Name Lookup(名称查找)。
这一阶段,会根据调用的函数名称,查找函数的所有声明。若函数具有唯一的名称,那么就不会触发重载决议;若查找到多个相同的函数名称,这些函数声明就会被视为一个 overload set(重载集)。
函数又分为普通函数和函数模板,在 Name Lookup 阶段都会被查找到。但是函数模板只有实例化之后才能被使用,因此如果存在函数模板,还需要对模板进行特殊的处理,这个阶段就称为Template Handling(模板处理)。
经过上述两个阶段的处理,得到的重载集就称为 candidate functions(候选函数),重载决议的工作就是在这些 candidate functions 中,找出最适合的那一个函数。
总结一下,当你调用一个重载函数时,编译器首先会进行 Name Lookup,找出所有函数声明,然后对函数模板进行 Template Handling,实例化出模板函数,产生 candidate functions,接着重载决议出现,找出最佳匹配函数。
而实际的最佳匹配函数调用,则是通过 Name mangling 产生的函数名称完成的。
1.2、Name Lookup
首先来看第一阶段,Name Lookup。该阶段仅仅进行名称查找,并不做任何额外检查。
Name Lookup 的工作主要可以分为两大部分。
第一部分为 Qualified Name Lookup(有修饰名称查找),这主要针对的是带有命名空间的函数调用,或是成员函数。
第二部分为 Unqualified Name Lookup(无修饰名称查找),这种针对的就是普通函数的调用。
下面依次进行讨论。
1.2.1、Qualified Name Lookup
带修饰的名称查找并不算复杂,这又可以主要分为两类查找。
一类是 Class Member Lookup,表示对于类成员的名称查找;另一类是 Namespace Member Lookup,表示对于命名空间下的名称查找。
其实还可以包含枚举名称,因为它也可以使用作用域解析操作符”::” 进行访问,但一法通万法,不必单独细论。
以下单独讨论主要的两类。
1.2.1.1、Class Member Lookup
类成员查找,是在访问类成员时进行名称查找的规则。
成员本质上来说还是两种类型,变量与函数。换个角度来看,成员又可分为静态成员和动态成员,静态成员可以通过”::” 进行访问,动态成员可以通过”.” 或”->” 进行访问。
也就是说,当你使用如上三种方式访问某个变量或函数时,就可能会触发 Class Member Lookup。
首先来看前者,即使用”::” 访问时的规则。示例如下:
class X {};
class C {class X {};static const int number = 50;static X arr[number];
};X C::arr[number]; // #1
可以将 #1 处的定义从”::” 拆分为前后两部分。
对于前面的名称 X 和 C,将会在其定义的命名空间进行查找,此处即为全局空间,于是查找到全局作用域下的 X 和 C 类。
对于后面的名称 arr 和 number,将会在 C 类的作用域下进行查找,它们将作为类成员进行查找。
此时就是”::” 前面的类型名,告诉编译器后面的名称应该通过 Class Member Lookup 进行查找。如果搜索发现前面是个命名空间,则会在相应的作用域下查找。
由于 X 是在全局作用域下查找到的,所以并不会找到内部类 X,于是该声明会产生编译错误。
接着来看后者,关于”.” 和”->” 的规则。看一个简单的例子:
struct S {void f() {}
};S s;
s.f();
S* ps = &s;
ps->f();
此处要调用 f 函数,因为使用了”.” 或”->” 操作符,而操作符前面又是一个类,所以 f 的查找将直接使用 Class Member Lookup,在类的作用域下进行查找。
这种调用一目了然,查找起来也比较方便,便不在此多加着墨,下面来看另一类带修饰的名称查找。
1.2.1.2、Namespace Member Lookup
命名空间成员查找,是在访问命名空间下的元素时进行名称查找的规则。
当你使用”::” 访问元素的时候,就有可能会触发 Namespace Member Lookup。
比如,当把”::” 单独作为前缀时,则会强制 Name Lookup 在全局空间下进行查找。如下述例子:
void f(); // #1namespace mylib {void f(); // #2void h() {::f(); // calls #1f(); // calls #2}
} // namespace mylib
此时,若是没有在全局作用域下搜索到相应的函数名称,也不会调用 #2,而是产生编译错误。若是要在外部访问命名空间内部的 f(),则必须使用 mylib::f(),否则 Name Lookup 会找到全局作用域下的 #1。
下面再来看一个稍微复杂点的例子:
int x;
namespace Y {void f(float);void h(int);
}namespace Z {void h(double);
}namespace A {using namespace Y;void f(int);void g(int);int i;
}namespace B {using namespace Z;void f(char);int i;
}namespace AB {using namespace A;using namespace B;void g();
}void h() {AB::g(); // #1AB::f(1); // #2AB::f('c'); // #3AB::x++; // #4AB::i++; // #5AB::h(16.8); // #6
}
这里一共有 6 处调用,下面分别来进行分析。
第一处调用,#1。
Name Lookup 发现 AB 是一个命名空间,于是在该空间下查找 g() 的定义,在 29 行查找成功,于是可以成功调用。
第二处调用,#2。
Name Lookup 同样先在 AB 下查找 f() 的定义,注意,查找的时候不会看参数,只看函数名称。
然而,在 AB 下未找到相关定义,可是它发现这里还有了两个 using-directives,于是接着到命名空间 A 和 B 下面查找。
之后,它分别查找到了 A::f(int) 和 B::f(char) 两个结果,此时重载决议出现,发现 A::f(int) 是更好的选择,遂进行调用。
第三处调用,#3。
它跟 #2 的 Name Lookup 流程完全相同,最终查找到了 A::f(int) 和 B::f(char)。于是重载决议出现,发现后者才是更好的选择,于是调用 B::f(char)。
第四处调用,#4。
Name Lookup 先在 AB 下查找 x 的定义,没有找到,于是再到命名空间 A 和 B 下查找,依旧没有找到。可是它发现 A 和 B 中也存在 using-directives,于是再到命名空间 Y 和 Z 下面查找。然而,还是没有找到,最终编译失败。
这里它并不会去查找全局作用域下的 x,因为 x 的访问带有修饰。
第五处调用,#5。
Name Lookup 在 AB 下查找失败,于是转到 A 和 B 下面查找,发现存在 A::i 和 B::i 两个结果。但是它们的类型也是一样,于是重载决议失败,产生 ambiguous(歧义)的错误。
最后一处调用,#6。
同样,在 AB 下查找失败,接着在 A 和 B 下进行查找,依旧失败,于是接着到 Y 和 Z 下面查找,最终找到 Y::h(int) 和 Z::h(double) 两个结果。此时重载决议出现,发现后者才是更好的选择,于是最终选择 Z::h(double)。
通过这个例子,相信大家已经具备分析 Namespace Member Lookup 名称查找流程的能力。
接着再补充几个需要注意的点。
第一点,被多次查找到的名称,但是只有一处定义时,并不会产生 ambiguous。
namespace X {int a;
}namespace A {using namespace X;
}namespace B {using namespace X;
}namespace AB {using namespace A;using namespace B;
}AB::a++; // OK
这里,Name Lookup 最终查找了两次 X::a,但因为实际只存在一处定义,于是一切正常。
第二点,当查找到多个定义时,若其中一个定义是类或枚举,而其他定义是变量或函数,且这些定义处于同一个命名空间下,则后者会隐藏前者,即后者会被选择,否则 ambiguous。
可以通过以下例子来进行理解:
namespace A {struct x {};int x;int y;
}namespace B {struct y {};
}namespace C {using namespace A;using namespace B;int i = C::x; // #1int j = C::y; // #2
}
先看 #1,由于 C 中查找 x 失败,进而到 A 和 B 中进行查找,发现 A 中有两处定义。一处定义是类,另一处定义是变量,于是后者隐藏前者,最终选择 int x; 这处定义。
而对于 #2,最终查找到了 A::y 和 B::y 两处定义,由于定义不在同一命名空间下,所以产生ambiguous。
到此,对 Qualified Name Lookup 的内容就基本覆盖了,下面进入 Unqualified Name Lookup。
1.2.2、Unqualified Name Lookup
无修饰的名称查找则略显复杂,却会经常出现。
总的来说,也可分为两大类。
第一类为 Usual Unqualified Lookup,即常规无修饰的名称查找,也就是普遍情况会触发的查询。
第二类为 Argument Dependant Lookup,这就是鼎鼎大名的 ADL,译为实参依赖查找。由其甚至发展出了一种定制点表示方式,称为 ADL 二段式,标准中的 std::swap, std::begin, std::end, operator«等等组件就是通过该法实现的。
但是本文并不会涉及定制点的讨论,因为这是我正在写的书中的某一节内容:) 内容其实非常之多之杂,本篇文章其实就是为该节扫除阅读障碍而特意写的,侧重点并不同。我额外写过一篇介绍定制点的文章【使用 Concepts 表示变化「定制点」】,各位可作开胃菜。
以下两节,分别讲解这两类名称查找。
1.2.2.1、Usual Unqualified Lookup
普通的函数调用都会触发 Usual Unqualified Lookup,先看一个简单的例子:
void f(char);void f(double);namespace mylib {void f(int);void h() {f(3); // #1f(.0); // #2}
}
对于 #1 和 #2,Name Lookup 会如何查找?最终会调用哪个重载函数?
实际上只会查找到 f(int),#1 直接调用,#2 经过了隐式转换后调用。
为什么呢?记住一个准则,根据作用域查找顺序,当 Name Lookup 在某个作用域找到声明之后,便会停止查找。关于作用域的查找顺序,后面会介绍。
因此,当查找到 f(int),它就不会再去全局查找其他声明。
注意:即使当前查找到的名称实际无法成功调用,也并不改变该准则。看如下例子:
void f(int);namespace mylib {void f(const char*);void h() {f(3); // #1 Error}
}
此时,依旧只会查找到 f(const char*),即使 f(int) 才是正确的选择。由于没有相应的隐式转换,该代码最终编译失败。
那么具体的作用域查找顺序是怎样的?请看下述例子:
namespace M {class B { // S3};
}// S5
namespace N {// S4class Y : public M::B {// S2class X {// S1int a[i]; // #1};};
}
#1 处使用了变量 i,因此 Name Lookup 需要进行查找,那么查找顺序将从 S1-S5。所以,只要在 S1-S5 的任何一处声明该变量,就可以被 Name Lookup 成功找到。
接着来看另一个查找规则,如果一个命名空间下的变量是在外部重新定义的,那么该定义中涉及的其他名称也会在对应的命名空间下查找。
简单的例子:
namespace N {int i = 4;extern int j;
}int i = 2;
int N::j = i; // j = 4
由于 N::j 在外部重新定义,因此变量 i 也会在命名空间 N 下进行查找,于是 j 的值为 4。如果在 N 下没有查找到,才会查找到全局的定义,此时 j 的值为 2。
而对于友元函数,查找规则又不相同,看如下例子:
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); // #1friend void A::f2(BT); // #2friend void A::f3<AT>(); // #3
};
此处,#1 的 AT 查找到的是 A::AT,#2 的 BT 查找到的是 B::BT,而 #3 的 AT 查找到的是 B::AT。
这是因为,当查找的名称并非模板参数时,首先会在友元函数的原有作用域进行查找,若没查找到,则再在当前作用域进行查找。对于模板参数,则直接在当前作用域进行查找。
1.2.2.2、Argument Dependant Lookup
终于到了著名的 ADL,这是另一种无修饰名称查找方式。
什么是 ADL?其实概念很简单,看如下示例。
namespace mylib {struct S {};void f(S);
}int main() {mylib::S s;f(s); // #1,OK
}
按照 Usual Unqualified Lookup 是无法查找到 #1 处调用的声明的,此时编译器就要宣布放弃吗?并不会,而是再根据调用参数的作用域来进行查找。此处,变量 s 的类型为 mylib::S,于是将在命名空间 mylib 下继续查找,最终成功找到声明。
由于这种方式是根据调用所依赖的参数进行名称查找的,因此称为实参依赖查找。
那么有没有办法阻止 ADL 呢?其实很简单。
namespace mylib {struct S {};void f(S) {std::cout << "f found by ADL\n";}
}void f(mylib::S) {std::cout << "global f found by Usual Unqualified Lookup\n";
}int main() {mylib::S s;(f)(s); // OK, calls global f
}
这里存在两个定义,本应产生歧义,但当你给调用名称加个括号,就可以阻止 ADL,从而消除歧义。
实际上,ADL 最初提出来是为了简化重载调用的,可以看如下例子。
int main() {// std::operator<<(std::ostream&, const char*)// found by ADL.std::cout << "dummy string\n";// same as aboveoperator<<(std::cout, "dummy string\n");
}
如果没有 ADL,那么 Unqualified Name Lookup 是无法找到你所定义的重载操作符的,此时你只能写出完整命名空间,通过 Qualified Name Lookup 来查找到相关定义。
但这样代码写起来就会非常麻烦,因此,Unqualified Name Lookup 新增加了这种 ADL 查找方式。
在编写一个数学库的时候,其中涉及大量的操作符重载,此时 ADL 就尤为重要,否则像是”+”,”==” 这些操作符的调用都会非常麻烦。
后来 ADL 就被广泛运用,普通函数也支持此种查找方式,由此还诞生了一些奇技淫巧。
不过,在说此之前,让我们先熟悉一下常用的 ADL 规则,主要介绍四点。
第一点,当实参类型为函数时,ADL 会根据该函数的参数及返回值所属作用域进行查找。
例子如下:
namespace B {struct R {};void g(...) {std::cout << "g found by ADL\n";}
}
namespace A {struct S {};typedef B::R (*pf)(S);void f(pf) {std::cout << "f found by ADL\n";}
}B::R bar(A::S) {return {};
}int main() {A::pf fun = bar;f(fun); // #1, OKg(fun); // #2, OK
}
#1 和 #2 处,分别调用了两个函数,参数为另一个函数,根据该条规则,ADL 得以查找到 A::f()与 B::g()。
第二点,若实参类型是一个类,那么 ADL 会从该类或其父类的最内层命名空间进行查找。
例子如下:
namespace A {// S2struct Base {};
}namespace M {// S3 not works!namespace B {// S1struct Derived : A::Base {};}
}int main() {M::B::Derived d;f(d); // #1
}
此处,若要通过 ADL 找到 f() 的定义,可以将其声明放在 S1 或 S2 处。
第三点,若实参类型是一个类模板,那么 ADL 会在特化类的模板参数类型的命名空间下进行查找;若实参类型包含模板模板参数,那么 ADL 还会在模板模板参数类型的命名空间下查找。
例子如下:
namespace C {struct Final {};void g(...) {std::cout << "g found by ADL\n";}
};
namespace B {template <typename T>struct Temtem {};struct Bar {};void f(...) {std::cout << "f found by ADL\n";}
}
namespace A {template <typename T>struct Foo {};
}int main() {// class template argumentsA::Foo<B::Bar> foo;f(foo); // OK// template template argumentsA::Foo<B::Temtem<C::Final>> a;g(a); // OK
}
代码一目了然,不多解释。
第四点,当使用别名时,ADL 会无效,因为名称并不是一个函数调用。
看这个例子:
typedef int f;
namespace N {struct A {friend void f(A&);operator int();void g(A a) {int i = f(a); // #1}};
}
注意 #1 处,并不会应用 ADL 来查询函数 f,因为它其实是 int,相当于调用 int(a)。
说完了这四点规则,下面来稍微说点 ADL 二段式相关的内容。
看下面这个例子:
namespace mylib {struct S {};void swap(S&, S&) {}void play() {using std::swap;S s1, s2;swap(s1, s2); // OK, found by Unqualified Name Lookupint a1, a2;swap(a1, a2); // OK, found by using declaration}
}
然后,你要在某个地方调用自己提供的这个定制函数,此处是 play() 当中。
但是调用的地方,你需要的 swap() 可能不只是定制函数,还包含标准中的版本。因此,为了保证调用形式的唯一性,调用被分成了两步。
- 使用 using declaration
- 使用 swap()
这样一来,不同的调用就可以被自动查找到对应的版本上。然而,只要稍微改变下调用形式,
代码就会出错:
namespace mylib {struct S {};void swap(S&, S&) {} // #1void play() {using namespace std;S s1, s2;swap(s1, s2); // OK, found by Unqualified Name Lookupint a1, a2;swap(a1, a2); // Error}
}
这里将 using declaration 写成了 using directive,为什么就出错了?
其实,前者将 std::swap() 直接引入到了局部作用域,后者却将它引入了与最近的命名空间同等的作用域。根据前面讲过的准则:根据作用域查找顺序,当 Name Lookup 在某个作用域找到声明之后,便会停止查找。编译器查找到了 #1 处的定制函数,就立即停止,因此通过 using directive 引入的 std::swap() 实际上并没有被 Name Lookup 查找到。
这个细微的差异很难发现,标准在早期就犯了这个错误,因此 STL 中的许多实现存在不少问题,但由于 ABI 问题,又无法直接修复。这也是 C++20 引入 CPOs 的原因,STL2 Ranges 的设计就采用了这种新的定制点方式,以避免这个问题。
在这之前,标准发明了另一种方式来解决这个问题,称为 Hidden friends。
namespace mylib {struct S {// Hidden friendsfriend void swap(S&, S&) {}};void play() {using namespace std;S s1, s2;swap(s1, s2); // OK, found by ADLint a1, a2;swap(a1, a2); // OK}
}
就是将定制函数定义为友元版本,放在类的内部。此时将不会再出现名称被隐藏的问题,这个函数只能被 ADL 找到。
Hidden friends 的写法在 STL 中存在不少,想必大家曾经也不知不觉中使用过。
好,更多关于定制点的内容本文不再涉及,下面进行另一个内容。
1.2.3、Template Name Lookup
以上两节 Name Lookup 内容只涉及零星关于模板的名称查找,本节专门讲解这部分查找,它们还是属于前两节的归类。
首先要说的是对于 typename 的使用,在模板当中声明一些类型,有些地方并不假设其为类型,此时只有在前面添加 typename,Name Lookup 才视其为类型。
不过自 C++20 之后,需要添加 typename 的地方已越来越少。
其次,介绍一个非常重要的概念,「独立名称」与「依赖名称」。
什么意思呢?看一个例子。
int j;template <class T>
struct X {void f(T t, int i, char* p) {t = i; // #1p = i; // #2p = j; // #3}
};
在 Name Lookup 阶段,模板还没有实例化,因此此时的模板参数都是未知的。对于依赖模板参数的名称,就称其为「依赖名称」,反之则为「独立名称」。
依赖名称,由于 Name Lookup 阶段还未知,因此对其查找和诊断要晚一个阶段,到模板实例化阶段。
独立名称,其有效性则在模板实例化之前,比如 #2 和 #3,它们诊断就比较早。这样,一旦发现错误,就不必再继续向下编译,节省编译时间。
查找阶段的变化对 Name Lookup 存在影响,看如下代码:
void f(char);template <class T>
void g(T t) {f(1); // non-dependentf(T(1)); // dependentf(t); // dependentdd++; // non-dependent
}enum E { e };
void f(E);double dd;
void h() {g(e); // calls f(char),f(E),f(E)g('a'); // calls f(char),f(char),f(char)
}
在 h() 里面有两处对于 g() 的调用,而 g() 是个函数模板,于是其中的名称查找时间并不相同。
f(char) 是在 g() 之前定义的,而 f(E) 是在之后定义的,按照普通函数的 Name Lookup,理应是找不到 f(E) 的定义的。
但因为存在独立名称和依赖名称,于是独立名称会先行查找,如 f(1) 和 dd++,而变量 dd 也是
在 g() 之后定义的,所以无法找到名称,dd++ 编译失败。对于依赖名称,如 f(T(1)) 和 f(t),它们则是在模板实例化之后才进行查找,因此可以查找到 f(E)。
一言以蔽之,即使把依赖名称的定义放在调用函数之后,由于其查找实际上发生于实例化之后,故也可成功找到。
事实上,存在术语专门表示此种查找方式,称为 Two-phase Name Lookup(二段名称查找),在下节还会进一步讨论。
接着来看一个关于类外模板定义的查找规则。
看如下代码:
template <class T>
struct A {struct B {};typedef void C;void f();template<class U> void g(U);
};template <class B>
void A<B>::f() {B b; // #1
}template <class B>
template <class C>
void A<B>::g(C) {B b; // #2C c; // #3
}
思考一下,#1,#2,#3 分别分别查找到的是哪个名称?(这个代码只有 clang 支持)
实际上,#1 和 #2 最终查找到的都是 A::B,而 #3 却是模板参数 C。
注意第 16-17 行出现的两个模板,它们并不能合并成一个,外层模板指的是类模板,而内层模板指的是函数模板。
因此,规则其实是:对于类外模板定义,如果成员不是类模板或函数模板,则类模板的成员名称会隐藏类外定义的模板参数;否则模板参数获胜。
而如果类模板位于一个命名空间之内,要在命名空间之外定义该类模板的成员,规则又不相同。
namespace N {class C {};template <class T> class B {void f(T);};
}template <class C>
void N::B<C>::f(C) {C b; // #1
}
此处,#1 处的 C 查找到的是模板参数。
如果是继承,那么也会隐藏模板参数,代码如下:
struct A {struct B {};int a;int Y;
};template <class B, class a>
struct X : A {B b; // A::Ba b; // A::a, error, not a type name
};
这里,最终查找的都是父类当中的名称,模板参数被隐藏。
然而,如果父类是个依赖名称,由于名称查找于模板实例化之前,所以父类当中的名称不会被考虑,代码如下:
typedef double A;
template <class T>
struct B {typedef int A;
};template <class T>
struct X : B<T> {A a; // double
};
这里,最终 X::A 的类型为 double,这是识别为独立名称并使用 Unqualified Name Lookup 查找到的。若要访问 B::A,那么声明改为 B::A a; 即可,这样一来就变为了依赖名称,且采用 Qualified Name Lookup 进行查找。
最后,说说多继承中包含依赖名称的规则。
还是看一个例子:
struct A {int m;
};struct B {int m;
};template <class T>
struct C : A, T {int f() { return this->m; } // #1int g() { return m; } // #2
};template int C<B>::f(); // ambiguous!
template int C<B>::g(); // OK
此处,多重继承包含依赖名称,名称查找方式并不相同。
对于 #1,使用 Qualified Name Lookup 进行查找,查询发生于模板实例化,于是存在两个实例,出现 ambiguous。
而对于 #2,使用 Unqualified Name Lookup 进行查找,此时相当于是独立名称查找,查找到的只有 A::m,所以不会出现错误。
1.2.4、Two-phase Name Lookup
因为模板才产生了独立名称与依赖名称的概念,依赖名称的查找需要等到模板实例化之后,这就是上节提到的二段名称查找。
依赖名称的存在导致 Unqualified Name Lookup 失效,此时,只有使用 Qualified Name Lookup 才能成功查找到其名称。
举个非常常见的例子:
struct Base {// non-dependent namevoid f() {std::cout << "Base class\n";}
};
struct Derived : Base {// non-dependent namevoid h() {std::cout << "Derived class\n";f(); // OK}
};int main() {Derived d;d.h();
}// Outputs:
// Derived class
// Base class
这里,f() 和 h() 都是独立名称,因此能够通过 Unqualified Name Lookup 成功查找到名称,程序一切正常。
然而,把上述代码改成模板代码,情况就大不相同了。
template <typename T>struct Base {void f() {std::cout << "Base class\n";}
};template <typename T>
struct Derived : Base<T> {void h() {std::cout << "Derived class\n";f(); // error: use of undeclared identifier 'f'}
};int main() {Derived<int> d;d.h();
}
此时,代码已经无法编译通过了。
为什么呢?当编译器进行 Name Lookup 时,发现 f() 是一个独立名称,于是在模板定义之时就开始查找,然而很可惜,没有查找到任何结果,于是出现未定义的错误。
那么它为何不在基类当中查找呢?这是因为它的查找发生在第一阶段的 Name Lookup,此时模板还没有实例化,编译器不能草率地在基类中查找,这可能导致查找到错误的名称。
更进一步的原因在于,模板类支持特化和偏特化,比如我们再添加这样的代码:
template <>
struct Base<char> {void f() {std::cout << "Base<char> class\n";}
};
若是草率地查找基类中的名称,那么查找到的将不是特化类当中的名称,查找出错。所以,在该阶段编译器不会在基类中查找名称。
那么,如何解决这个问题呢?
有两种办法,代码如下:
template <typename T>
struct Derived : Base<T> {void h() {std::cout << "Derived class\n";this->f(); // method 1Base<T>::f(); // method 2}
};
这样一来,编译器就能够成功查找到名称。
原理是这样的:通过这两种方式,就可以告诉编译器该名称是依赖名称,必须等到模板实例化之后才能进行查找,届时将使用 Qualified Name Lookup 进行查找。这就是二段名称查找的必要性。
在调用类函数模板时依旧存在上述问题,一个典型的例子:
struct S {template <typename T>static void f() {std::cout << "f";}
};template <typename T>
void g(T* p) {T::f<void>(); // #1 error!T::template f<void>(); // #2 OK
}int main() {S s;g(&s);
}
此处,由于 f() 是一个函数模板,#1 的名称查找将以失败告终。
因为它是一个依赖名称,编译器只假设名称是一个标识符(比如变量名、成员函数名),并不会认为它们是类型或函数模板。
原因如前面所说,由于模板特化和偏特化的存在,草率地假设会导致名称查找错误。此时,就需要显式地告诉编译器它们是一个类型或是函数模板,告诉编译器如何解析。
这也是需要对类型使用 typename 的原因,而对于函数模板,则如 #2 那样添加一个 template,这样就可以告诉编译器这是一个函数模板,<> 当中的名称于是被解析为模板参数。
#1 失败的原因也显而亦见,编译器将 f() 当成了成员函数,将 <> 解析为了比较符号,从而导致编译失败。
至此,关于 Name Lookup 的内容就全部结束了,下面进入重载决议流程的第二阶段——模板处理。
待续
相关文章:
C++学习笔记--函数重载(1)
文章目录 序言一、洞悉函数重载决议1.1、重载决议的基本流程1.2、Name Lookup1.2.1、Qualified Name Lookup1.2.1.1、Class Member Lookup1.2.1.2、Namespace Member Lookup 1.2.2、Unqualified Name Lookup1.2.2.1、Usual Unqualified Lookup1.2.2.2、Argument Dependant Look…...
交叉编译poco-1.9.2
目录 一、文件下载二、编译三、可能遇到的问题和解决方法3.1 error "Unknown Hardware Architecture."3.2 error Target architecture was not detected as supported by Double-Conversion一、文件下载 下载地址:poco-1.9.2 二、编译 解压目录后打开build/config/…...
C++中如何处理超长的数字(long long类型的整数都无法存储的)
C中如何处理超长的数字(long long类型的整数都无法存储的) 在 C中,如果数字超出了 long long 类型的范围,可以考虑使用字符串或第三方库(如 Boost.Multiprecision)来表示和处理超长数字。要使用第三方库需…...
RabbitMQ MQTT集群方案官方说明
RabbitMQ MQTT 官方网说明 官方地址: https://www.rabbitmq.com/mqtt.html 从3.8开始,该MQTT插件要求存在一定数量的群集节点。这意味着三分之二,五分之三,依此类推。 该插件也可以在单个节点上使用,但不支持两个节点的集群。 如…...
深圳唯创知音电子将参加IOTE 2023第二十届国际物联网展•深圳站
2023年9月20~22日,深圳唯创知音电子将在 深圳宝安国际会展中心(9号馆9B1)为您全面展示最新的芯片产品及应用方案,助力传感器行业的发展。 作为全球领先的芯片供应商之一,深圳唯创知音电子一直致力于为提供高质量、…...
《TCP/IP网络编程》阅读笔记--I/O复用
目录 1--基于I/O复用的服务器 2--select()函数 3--基于I/O复用的回声服务器端 4--send()和recv()函数的常用可选项 5--readv()和writev()函数 1--基于I/O复用的服务器 多进程服务器端具有以下缺点:当有多个客户端发起连接请求时,就会创建多个进程来…...
[C#] 允许当前应用程序通过防火墙
通常在一台装有防火墙的电脑上运行程序的场合,往往会弹出对话框提示:是否允许执行该应用程序。 我们在开发软件的时候,可以事先在软件里面设置当前软件为防火墙允许通过的软件。这样,用户在使用时就可以避开前面提到的弹框了。 在…...
帆软FineReport决策报表Tab实现方案
最近有个需求是要做首页展示,为了减少前端工作量,利用采购的帆软FineReport来实现,记录过程,方便备查。 需求 做个Tab页,实现多个页切换。 方案一、利用帆软自带切换 帆软自带的有Tab控件,可实现切换&a…...
只打印文名
CMakeLists.txt set(CMAKE_C_FLAGS "-O0 -ggdb -D__NOTDIR_FILE__$(notdir $<)") // set(CMAKE_C_FLAGS "-O0 -ggdb -D__NOTDIR_FILE__$(notdir $<) -D__FILENAME__$(subst $(dir $<),,$<)")C文件 #include <stdio.h>#ifdef __NOTDIR_…...
【经典小练习】JavaSE—拷贝文件夹
🎊专栏【Java小练习】 🍔喜欢的诗句:天行健,君子以自强不息。 🎆音乐分享【如愿】 🎄欢迎并且感谢大家指出小吉的问题🥰 文章目录 🎄效果🌺代码🛸讲解&#x…...
FPGA-结合协议时序实现UART收发器(六):仿真模块SIM_uart_drive_TB
FPGA-结合协议时序实现UART收发器(六):仿真模块SIM_uart_drive_TB 仿真模块SIM_uart_drive_TB,仿真实现。 vivado联合modelsim进行仿真。 文章目录 FPGA-结合协议时序实现UART收发器(六):仿真模…...
Spring Boot集成EasyExcel实现数据导出
在本文中,我们将探讨如何使用Spring Boot集成EasyExcel库来实现数据导出功能。我们将学习如何通过EasyExcel库生成Excel文件,并实现一些高级功能,如支持列下拉和自定义单元格样式,自适应列宽、行高,动态表头 ÿ…...
EasyExcel3.0读(日期、数字或者自定义格式转换)
EasyExcel 3.0读(日期、数字或者自定义格式转换) 依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.2.1</version> </dependency>对象 package com.xiaobu.entity.vo;import …...
浅谈C++|STL之vector篇
一.vector的基本概念 vector是C标准库中的一种动态数组容器,提供了动态大小的数组功能,能够在运行时根据需要自动扩展和收缩。vector以连续的内存块存储元素,可以快速访问和修改任意位置的元素。 以下是vector的基本概念和特点: 动…...
微信、支付宝修改步数【小米运动】
简介 小米运动是一款流行的健身应用,可以记录用户的步数和运动数据。然而,有些用户希望能够修改步数,以达到一些特定的目的。本文将介绍一个Python脚本,可以帮助用户实现修改小米运动步数的功能。 正文 脚本介绍: 本脚本是一个Python脚本,用于修改小米运动步数。通过模…...
stu02-初识HTML
1.HTML概述 (1)HTML是Hyper Text Mark-up Language的首字母缩写。 (2)HTML是一种超文本标记语言。 (3) 超文本:指除了文字外,页面内还可以包含图片、链接、甚至音乐、视频等非文字元…...
软件测试7大误区
随着软件测试对提高软件质量重要性的不断提高,软件测试也不断受到重视。但是,国内软件测试过程的不规范,重视开发和轻视测试的现象依旧存在。因此,对于软件测试的重要性、测试方法和测试过程等方面都存在很多不恰当的认识…...
【深度学习】 Python 和 NumPy 系列教程(十二):NumPy详解:4、数组广播;5、排序操作
目录 一、前言 二、实验环境 三、NumPy 0、多维数组对象(ndarray) 多维数组的属性 1、创建数组 2、数组操作 3、数组数学 4、数组广播 5、排序操作 1. np.sort() 函数 2. np.argsort() 函数 3. ndarray.sort() 方法 4. 按列或行排序 5. n…...
CSS宽度问题
一、魔法 为 DOM 设置宽度有哪些方式呢?最常用的是配置width属性,width属性在配置时,也有多种方式: widthmin-widthmax-width 通常当配置了 width 时,不会再配置min-width max-width,如果将这三者混合使…...
浅谈C++|STL之string篇
一.string的基本概念 本质 string是C风格的字符串,而string本质是一个字符串 string和char * 区别 char * 是一个指针string是一个类,类内部封装了char *,管理这个字符串,是一个char * 型容器。 特点 string类内部封装了很多成…...
Kubernetes Dashboard安装部署
Kubernetes Dashboard安装部署 1. 下载Dashboard 部署文件2. 修改yaml配置文件3. 应用安装,查看pod和svc4. 创建dashboard服务账户5. 创建admin-user用户的登录密钥6. 登录6.1 使用token登录(1) 短期token(2) token长期有效 6.2 使用 Kubeconfig 文件登录 7.安装met…...
在Qt的点云显示窗口中添加坐标轴C++
通过摸索整理了三个方法: 一、方法1://不推荐,但可以参考 1、通过pcl的compute3DCentroid()方法计算点云的中心点坐标; 函数原型如下: compute3DCentroid (const pcl::PointCloud<PointT> &cloud, Eigen…...
[密码学入门]凯撒密码(Caesar Cipher)
密码体质五元组:P,C,K,E,D P,plaintext,明文空间 C,ciphertext,密文空间 K,key,密钥空间 E,encrypt,加密算法 D,decrypt,解密算法 单表代换…...
uboot 顶层Makefile-make xxx_deconfig过程说明三
一. uboot 的 make xxx_deconfig配置 本文接上一篇文章的内容。地址如下:uboot 顶层Makefile-make xxx_deconfig过程说明二_凌肖战的博客-CSDN博客 本文继续来学习 uboot 源码在执行 make xxx_deconfig 这个配置过程中,顶层 Makefile有关的执行思路。 …...
c++中的多线程通信
信息传递 #include <iostream> #include <thread> #include <chrono> #include <mutex> #include <condition_variable> #include <queue> // 用于存储和同步数据的结构 struct Data {std::queue<std::string> messag…...
IO day7
1->x.mind 2-> A进程 B进程...
C语言之指针进阶篇(3)
目录 思维导图 回调函数 案例1—计算器 案例2—qsort函数 关于qsort函数 演示qsort函数的使用 案例3—冒泡排序 整型数据冒泡排序 回调函数搞定各类型冒泡排序 cmp_int比较大小 cmp传参数 NO1. NO2. 解决方案 交换swap 总代码 今天我们学习指针难点之回调函数…...
SQL7 查找年龄大于24岁的用户信息
描述 题目:现在运营想要针对24岁以上的用户开展分析,请你取出满足条件的设备ID、性别、年龄、学校。 用户信息表:user_profile iddevice_idgenderageuniversityprovince12138male21北京大学Beijing23214male复旦大学Shanghai36543female20…...
vite搭建vue3项目
参考视频 1.使用npm搭建vite项目,会自动搭建vue3项目 npm create vitelatest yarn create vite2.手动搭建vue3项目 创建一个项目名称的文件夹执行命令:npm init -y 快速的创建一个默认的包信息安装vite: npm i vite -D -D开发环境的依赖 安装vue,现在默认是vue3.…...
Qt中表格属性相关操作,调整表格宽度高度自适应内容等
1 表格列宽设置 利用Qt designer设计,可以通过改变表头的列宽从而保证内容不会被遮盖,输入空格的方式增加表头的长度,比如表头为"Value",则改成"Value ",可以扩展列默认的宽度,保证后面…...
电子商务网站建设的结论/淘宝指数查询官网
1、按本站或各区域城市组织各位妈妈们在网站以宝宝照片经验交流形式SHOW自己可爱的宝宝,可按月、季、年设擂台,当然各擂主要设奖品:),奖品以后可由各赞助商提供; 2、宝宝SHOW分类: 2.1、【Bab…...
网站建设 申请/产品推广平台有哪些
IT行业是个大范围,小编仅从自己熟悉的软件测试领域来略加分析,说一点自己的思考,欢迎一起探讨。 软件测试工程师指:理解产品的功能要求,并对其进行测试,检查软件有没有错误(Bug)&am…...
小学学校网站设计模板/2024年最新时政热点
开源的数据库控制器 在开发中,我们经常会遇到上线数据库表的情况,代码上我们有git,svn这样优秀的版本控制软件,但是数据库的迭代我们不能使用手工的方式迭代吧?或者说每次上线前手工去数据库执行。这样带来的便捷性就会…...
手机版网站制作费用/电商平台怎么注册
概述 Scroller 译为“滚动器”,是 ViewGroup 类中原生支持的一个功能。我们经常有这样的体验:打开联系人,手指向上滑动,联系人列表也会跟着一起滑动,但是,当我们松手之后,滑动并不会因此而停止…...
学做网站论坛vip码/网站排名优化培训哪家好
思路1 遍历棋盘中的每一个位置,对于空位置,把1-9都往里面填,假设当前填入1没有打破条件(横向没有重复的点,纵向没有重复的点,方格里也没重复的点),那么当前位置就填1,再…...
贵阳培训网站建设/宣传推广方案范文
图片 Jietu20190404-1730282x.jpg介绍 Large jug with four grape-clusters symmetrically painted around the body grapes are a characteristic decorative motif in Theran pottery. 绘有四对象葡萄串的大水罐。 公元前16世纪 Akroteri on Thera 16th cent. BC. 年代&#…...