重载运算与类型转换——函数调用运算符,重载、类型转换与运算符

一、函数调用运算符

  如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通的函数相比它们更加灵活。

 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 }
View Code

即使absobj只是一个对象而非函数,我们也能“调用”该对象。调用对象实际上是在运行重载的调用运算符。在此例中,该运算符接受一个int值并返回其绝对值。

  函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

  如果类定义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。

1)含有状态的函数对象类

  和其他类一样,函数对象类除了operator()之外也可以包含其他成员。函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。函数对象常常作为泛型算法的实参。

 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 }
View Code

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)不同类型可能具有相同的调用形式

  对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。

 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 };
View Code

上面这些可调用对象分别对其参数执行了不同的算术运算,尽管它们的类型各不相同,但是共享同一种调用形式:

  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类型所能够表示的对象的调用形式。

 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 }
View Code

3)重载的函数与function

  我们不能直接将重载函数的名字存入function类型的对象中,会产生二义性问题。解决方法是存储函数指针。

 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 }
View Code

  我们也能使用lambda来消除二义性。

 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 }
View Code

二、重载、类型转换与运算符

  一个实参调用的非显式构造函数定义了一种隐式的类型转换,这种构造函数将实参类型的对象转换成类类型。我们同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到这一点。转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也被称作用户定义的类型转换。

1、类型转换运算符

  类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:

  operator type() const;

其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该 类型能作为函数的返回类型。因此,我们不允许转换成数组或函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。

  类型转换运算符既没有显式的返回类型,也没用形参,而且必须定义成类的成员函数。类型转换运算符通常不应该改变转换对象的内容,因此类型转换运算符一般被定义成const成员。

1)定义含有类型转换类型的类

  因为类型转换运算符时隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参。同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。

 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 }
View Code

2)类型转换运算符可能产生意外结果

3)显式的类型转换运算符

  为了防止异常情况的发生,C++11新标准引入了显式的类型转换运算符。编译器通常不会将一个显式的类型转换运算符用于隐式类型转换。

 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 }
View Code

当类型转换运算符是显式的时,我们也能执行类型转换,不过必须通过显式的强制类型转换才可以。

  当表达式出现在下列位置时,显式的类型转换将被隐式的执行:

  • if、while及do语句的条件部分。
  • for语句的条件表达式。
  • 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象。
  • 条件运算符(?:)的条件表达式。

2、避免有二义性的类型转换

  如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则的话,我们编写的代码将很可能具有二义性。

  在两种情况下可能产生多重转换路径。第一种情况是两个类提供相同的类型转换:例如,当A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符时,我们就说它们提供了相同的类型转换。

  第二种情况是类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。最典型的例子是算术运算符,对于某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。

3、函数匹配与重载运算符

  重载的运算符也是重载函数。因此,通用的函数匹配规则同样适用于在给定的表达式中到底应该使用内置运算符还是重载的运算符。

  当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。

  当我们在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内。

猜你喜欢

转载自www.cnblogs.com/ACGame/p/10311994.html