一、函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通的函数相比它们更加灵活。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 5 class absInt { 6 public: 7 int operator()(int val)const { 8 return val < 0 ? -val : val; 9 } 10 }; 11 12 int main() 13 { 14 absInt absobj; // 含有函数调用运算符的对象 15 int val = absobj(-42); // 将-42传递给absobj.operator() 16 std::cout << val << std::endl; 17 return 0; 18 }
即使absobj只是一个对象而非函数,我们也能“调用”该对象。调用对象实际上是在运行重载的调用运算符。在此例中,该运算符接受一个int值并返回其绝对值。
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。
1)含有状态的函数对象类
和其他类一样,函数对象类除了operator()之外也可以包含其他成员。函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。函数对象常常作为泛型算法的实参。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 6 class PrintStr { 7 public: 8 PrintStr(std::ostream &o=std::cout, char c=' ') 9 :os(o),sep(c){} 10 void operator()(const std::string &s) const { os << s << sep; } 11 private: 12 std::ostream &os; 13 char sep; 14 }; 15 16 int main() 17 { 18 std::vector<std::string> vec = { "a","b","c" }; 19 for_each(vec.begin(), vec.end(), PrintStr(std::cout, '\n')); 20 return 0; 21 }
for_each的第3个参数是类型PrintStr的一个临时对象。
1、lambda是函数对象
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符。
默认情况下lambda不能改变它捕获的变量。因此在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda被声明为可变的,则调用运算符就不是const的。
1)表示lambda及相应捕获行为的类
当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时所引的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。
相反,通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝构造/移动函数则通常要视捕获的数据成员类型而定。
2、标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。这些类型定义在头文件functional中。这些类都被定义成模板的形式,我们可以为其指定具体的应用类型,这里的类型即调用运算符的形参类型。
标准库函数对象:
算术 | 关系 | 逻辑 |
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logical_not<Type> |
divides<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
表示运算符的函数对象常用来替换算法中的默认运算符。
3、可调用对象与function
C++语言中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
和其他对象一样,可调用的对象也有类型。例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定。
然而,两个不同类型的可调用对象却可能共享同一种调用形式。调用形式指明了调用返回的类型及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:
int(int, int)
是一个函数类型,它接受两个int、返回一个int。
1)不同类型可能具有相同的调用形式
对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 int add(int i, int j) { return i + j; } 2 3 auto mod = [](int i, int j) {return i % j; }; 4 5 class divide { 6 public: 7 int operator()(int i, int j) { 8 return i / j; 9 } 10 };
上面这些可调用对象分别对其参数执行了不同的算术运算,尽管它们的类型各不相同,但是共享同一种调用形式:
int(int, int)
2)标准库function类型
我们可以使用一个名为function的新标准库类型存储可调用对象,function定义在functional头文件中。
function定义的操作:
操作 | 说明 |
function<T> f; | f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同 |
function<T> f(nullptr); | 显式的构造一个空function |
function<T> f(obj); | 在f中存储可调用对象obj的副本 |
f | 将f作为条件:当f含有一个可调用对象时为真;否则为假 |
f(args) | 调用f中的对象,参数是args |
定义为function<T>的成员的类型 | |
result_type | 该function类型的可调用对象返回的类型 |
argument_type | 当T有一个或两个实参定义的类型。 如果T只有一个实参,则 argument_type是该类型的同义词; 如果T有两个实参,则first_argument_type和second_argument_type分别代表两个实参的类型 |
first_argument_type | |
second_argument_type |
function是一个模板,和我们使用的其他模板一样,当创建一个具体的function类型时我们必须提供额外的信息中。在此例中,所谓额外的信息是指该function类型所能够表示的对象的调用形式。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 7 int add(int i, int j) { return i + j; } 8 9 auto mod = [](int i, int j) {return i % j; }; 10 11 class divide { 12 public: 13 int operator()(int i, int j) { 14 return i / j; 15 } 16 }; 17 int main() 18 { 19 std::function<int(int, int)> f1 = add; 20 std::function<int(int, int)> f2 = divide(); 21 std::function<int(int, int)> f3 = [](int i, int j) {return i % j; }; 22 std::cout << f1(10, 2) << std::endl; 23 std::cout << f2(10, 2) << std::endl; 24 std::cout << f3(10, 2) << std::endl; 25 return 0; 26 }
3)重载的函数与function
我们不能直接将重载函数的名字存入function类型的对象中,会产生二义性问题。解决方法是存储函数指针。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 int add(int i, int j) { return i + j; } 9 double add(double i, double j) { return i + j; } 10 11 int main() 12 { 13 std::map<std::string, std::function<int(int, int)>> binops; 14 int(*fp)(int, int) = add; // 指针所指的add是接受两个int的版本 15 binops.insert({ "+",fp }); 16 return 0; 17 }
我们也能使用lambda来消除二义性。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 int add(int i, int j) { return i + j; } 9 double add(double i, double j) { return i + j; } 10 11 int main() 12 { 13 std::map<std::string, std::function<int(int, int)>> binops; 14 binops.insert({ "+",[](int a,int b) {return add(a,b); } }); 15 return 0; 16 }
二、重载、类型转换与运算符
一个实参调用的非显式构造函数定义了一种隐式的类型转换,这种构造函数将实参类型的对象转换成类类型。我们同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到这一点。转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也被称作用户定义的类型转换。
1、类型转换运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
operator type() const;
其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该 类型能作为函数的返回类型。因此,我们不允许转换成数组或函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。
类型转换运算符既没有显式的返回类型,也没用形参,而且必须定义成类的成员函数。类型转换运算符通常不应该改变转换对象的内容,因此类型转换运算符一般被定义成const成员。
1)定义含有类型转换类型的类
因为类型转换运算符时隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参。同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Demo { 9 public: 10 Demo(int _val=0):val(_val){} 11 // 转换成int 12 operator int()const { 13 std::cout << __FUNCTION__ << std::endl; 14 return val; 15 } 16 private: 17 std::size_t val; 18 }; 19 int main() 20 { 21 Demo d; 22 d + 2; // 将d隐式地转换成int 23 return 0; 24 }
2)类型转换运算符可能产生意外结果
3)显式的类型转换运算符
为了防止异常情况的发生,C++11新标准引入了显式的类型转换运算符。编译器通常不会将一个显式的类型转换运算符用于隐式类型转换。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Demo { 9 public: 10 Demo(int _val=0):val(_val){} 11 // 转换成int 12 explicit operator int()const { 13 std::cout << __FUNCTION__ << std::endl; 14 return val; 15 } 16 private: 17 std::size_t val; 18 }; 19 int main() 20 { 21 Demo d; 22 //d + 2; // 错误:此处需要隐式的类型转换,但是显式的 23 static_cast<int>(d) + 2; // 正确:显式地请求类型转换 24 return 0; 25 }
当类型转换运算符是显式的时,我们也能执行类型转换,不过必须通过显式的强制类型转换才可以。
当表达式出现在下列位置时,显式的类型转换将被隐式的执行:
- if、while及do语句的条件部分。
- for语句的条件表达式。
- 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象。
- 条件运算符(?:)的条件表达式。
2、避免有二义性的类型转换
如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则的话,我们编写的代码将很可能具有二义性。
1)实参匹配和相同的类型转换
两个类提供相同的类型转换:例如,当A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符时,我们就说它们提供了相同的类型转换。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class B; 9 class A { 10 public: 11 A(int _x = 0):x(_x){} 12 A(const B &); // 把B转换成A 13 public: 14 int x; 15 }; 16 17 class B { 18 public: 19 B(int _x = 0):x(_x){} 20 operator A() const; // 把B转换成A 21 public: 22 int x; 23 }; 24 25 A::A(const B &item):x(item.x){} 26 27 B::operator A()const { 28 return A(x); 29 } 30 31 A func(const A &item) { 32 std::cout << __FUNCTION__ << std::endl; 33 } 34 int main() 35 { 36 B b; 37 A a = func(b); // 二义性错误:含义是f(B::operator A())还是f(A::A(const B &)) 38 return 0; 39 }
2)二义性与转换目标为内置类型的多重类型转换
如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性问题。最简单的例子就是类中定义了多个参数都是算术类型的构造函数,或者转换目标都是算术类型的类型转换运算符。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 public: 10 A(int _x=0):x(_x){} // 最好不要创建两个转换源都是算术类型的类型转换 11 A(double _x):x(_x){} 12 operator int()const { // 最好不要创建两个转换对象都是算术类型的类型转换 13 return x; 14 } 15 operator double()const { 16 return x; 17 } 18 public: 19 int x; 20 }; 21 void f(long double x) { 22 std::cout << __FUNCTION__ << std::endl; 23 } 24 int main() 25 { 26 A a; 27 f(a); // 二义性错误:是f(A::operator int())还是f(A::operator double()) 28 long double lg = 0; 29 A a2(lg); // 二义性错误:是A::A(int)还是A::A(double) 30 return 0; 31 }
在对f的调用中,哪个类型都无法精确匹配到long double。然而这两个类型转换都可以使用,只要后面再执行一次生成long double的标准类型转换即可。因此,在上面的两个类型转换中哪个都不比另一个更好,调用将会产生二义性。
当我们试图用long double初始化a2时也遇到了同样的问题。它们在使用构造函数之前都要求先进行类型转换,再执行构造函数,编译器没办法区分这两种转换序列的好坏,因此该调用产生二义性。
当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。
3)重载函数与转换构造函数
当我们调用重载的函数时,从多个类型转换中进行选择将变得更加复杂。如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。
举个例子,当几个重载函数的参数分属于不同的类类型时,如果这些类恰好定义了同样的转换构造函数,则二义性问题进一步提升:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class C { 9 public: 10 C(int _x):x(_x){} 11 public: 12 int x; 13 }; 14 class D { 15 public: 16 D(int _x):x(_x){} 17 public: 18 int x; 19 }; 20 21 void f(const C &item) { 22 std::cout << "C" << std::endl; 23 } 24 25 void f(const D &item) { 26 std::cout << "D" << std::endl; 27 } 28 int main() 29 { 30 f(1024); // 二义性错误:是f(C(1024))还是f(D(1024)) 31 return 0; 32 }
其中C和D的包含接受int的构造函数,两个构造函数各自匹配f的一个版本,因此该调用具有二义性。
4)重载函数与用户定义的类型转换
当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,则我们认为这些类型转换一样好。在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别。只有当重载函数能通过同一个类的类型转换函数得到匹配时,我们才会考虑其中出现的标准类型转换。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class C { 9 public: 10 C(int _x):x(_x){} 11 public: 12 int x; 13 }; 14 class D { 15 public: 16 D(double _x):x(_x){} 17 public: 18 int x; 19 }; 20 21 void f(const C &item) { 22 std::cout << "C" << std::endl; 23 } 24 25 void f(const D &item) { 26 std::cout << "D" << std::endl; 27 } 28 int main() 29 { 30 f(1024); // 二义性错误:是f(C(1024))还是f(D(1024)) 31 return 0; 32 }
在此例中,C有一个转换源为int的类型转换,D有一个转换源为double的类型转换,对于f(1024)来说,两个f函数都是可行的:
- f(const C&)是可行的,因为C有一个接受int的转换构造函数,该构造函数与实参精确匹配。
- f(const D&)是可行的,因为D有一个接受double的转换构造函数,而为了使用该函数我们可以利用标准类型转换把int转换成所需的类型。
因为调用重载函数所请求的用户类型转换不止一个且彼此不同,所以该调用具有二义性。即使其中一个调用需要额外的标准类型转换而另一个调用能精确匹配,编译器也会将该调用标示为错误。
3、函数匹配与重载运算符
重载的运算符也是重载函数。因此,通用的函数匹配规则同样适用于在给定的表达式中到底应该使用内置运算符还是重载的运算符。当运算符函数出现在表达式中时,候选函数集的规模很大。如果a是一种类类型,则表达式 a sym b可能是:
a.operatorsym(b); // a有一个operatorsym成员函数
operatorsym(a, b); // operatorsym是一个普通函数
和普通函数调用不同,我们不能通过调用的形式来区分当前调用的是成员函数还是非成员函数。
当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 friend A operator+(const A&, const A&); 10 public: 11 A(int _x=0):x(_x){} 12 operator int()const { 13 return x; 14 } 15 public: 16 int x; 17 }; 18 19 A operator+(const A &lhs, const A &rhs) { 20 return A(lhs.x + rhs.x); 21 } 22 int main() 23 { 24 A a1, a2; 25 A a3 = a1 + a2; // 使用重载的operator+ 26 int res = a3 + 0; // 二义性错误 27 return 0; 28 }
第2条加法语句具有二义性:我们可以把0转换成A,然后使用A的+;或者把a3转换成int,然后对两个int执行内置的加法运算。