C++重载运算:函数调用运算符

函数调用运算符

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

​ 下面名为 absInt 的 struct 含有一个调用运算符,该运算符返回其参数的绝对值。

struct absInt {
    int operator() (int val) const {
        return val < 0 ? -val : val;
    }
};

​ 我们使用调用运算符的方式是令一个 absInt 对象作用于一个实参列表,这一过程看起来非常像调用函数的过程:

int i = -42;
absInt absObj;
int ui = absOjb(i);		// 将 i 传递给 absObj.operator()

即使 absObj 只是一个对象而非函数,我们也能“调用”该对象。调用对象实际上是在运行重载的调用运算符。

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

​ 如果类定义了调用运算符,则该类的对象称作函数对象

含有状态的函数对象类

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

​ 举个例子,我们将定义一个打印 string 实参内容的类。默认情况下,我们的类会将内容写入到 cout 中,每个 string 之间以空格隔开。同时允许类的用户提供其他可写入的流及其他分隔符。我们将该类定义如下:

class PrintString {
public:
    PrintString(ostream &o = cout, char c = ' '):
    	os(o), sep(c) { }
    void operator() (const string&s) const { os << s << sep; }
private:
    ostream &os;	// 用于写入的目的流
    char sep;		// 用于将不同输出隔开的字符
};

​ 当定义 PrintString 的对象时,对于分隔符及输出流既可以使用默认值也可以提供我们自己的值:

PrintString printer;	// 使用默认值,打印到 cout
printer(s);				// 在 cout 中打印 s,后面跟一个空格
PrintString errors(cerr,'\n');
errors(s);				// 在 cerr 中打印 s,后面跟一个换行符

​ 函数对象常常作为泛型算法的实参。例如,可以使用标准库 for_each 和我们自己的 PrintString 类来打印容器内容:

for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
lambda 是函数对象

​ 上面,我们使用 PrintString 对象作为 for_each 的实参。我们也可以用 lambda 完成类似的操作。

​ 当我们编写一个 lambda 时,编译器将该表达式翻译成一个未命名类的未命名对象。在 lambda 表达式产生的类中含有一个重载的函数调用运算符。

​ 例如,我们传递给 stable_sort 作为最后一个实参的 lambda 表达式来说:

stable_sort(words.begin(),words.end(),
           [](const string &a,const string &b) 
            	{ return a.size() < b.size(); });

其行为类似于下面这个类的一个未命名对象:

class ShorterString {
public:
    bool operator() (const string &a,const string &b) const {
        return a.size() < b.size();
    }
};

我们知道,默认情况下,lambda 不能改变它捕获的变量。因此默认情况下,由 lambda 产生的类当中的函数调用运算符是一个 const 成员函数。如果 lambda 被声明为可变的,则调用运算符就不是 const 的。

​ 我们可以用 ShorterString 类代替 lambda:

stable_sort(words.begin(),words.end(),ShorterString);
表示 lambda 及相应捕获行为的类

​ 如我们所知,当一个 lambda 表达式通过引用捕获变量时,将由程序负责确保 lambda 执行时所引用的对象确实存在。因此,编译器可以直接使用该引用而无须在 lambda 产生的类中将其存储为数据成员

​ 相反,通过值捕获的变量被拷贝到 lambda 中。因此,这种 lambda 产生的类必须为每个值捕获的变量建立对于的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员

​ 假设我们有这样一条语句:

int sz; cin >> sz;
auto wc = find_if(words.begin(),words.end(),
                 [sz](const string &s) { return a.size() >= sz; });

该 lambda 表达式产生的类将形如:

class SizeComp {
public:
    SizeComp(size_t n): sz(n) { }
    bool operator() (const string &s) const
    	{ return s.size() >=sz; }
private:
    size_t sz;
};

所以,上面的语句可以用这个类来替代:

auto wc = find_if(words.begin(),words.end(),SizeComp(sz));

lambda 表达式产生的类不含默认构造函数,赋值运算符以及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常视要捕获的数据成员类型而定

标准库定义的函数对象

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例如,plus 类定义了一个函数调用运算符用于对一对运算对象执行 + 操作;modulus 类定义了一个调用运算符执行二元 % 操作;equal_to 类执行 ==,等等。

这些类都被定义成了模板的形式,我们可以为其指定具体的应用类型,这里的类型即调用运算符的形参类型。例如,plus<string> 令string 加法运算符作用于 string 对象;plus<int> 的运算对象是 int;plus<Sales_data> 对 Sales_data 执行加法运算,依次类推:

plus<int> intAdd;		// 可执行 int 加法的函数对像
negate<int> intNegate;	// 可对 int 值取反的函数对象(指求反数)
// 使用 intAdd::operator(int,int)求 10 和 20 的和
int sum = intAdd(10,20);				// sum = 30;
sum = intNegate(intAdd(10,20)); 		// sum = -30
// 使用 intNegate::operator(int) 生成 -10
// 然后将 -10 作为 intAdd::operator(int,int) 的第二个参数
sum = intAdd(10,intNegate(10));		// sum = 0

标准库函数对象有很多:如上述的 plus<Type>、modulus<Type>、negate<Type>、greater<Type>、less<Type> 等。可见 c++primer 5 p510.

在算法中使用标准库函数对象

表示运算符的函数对象类常用来替代算法中的默认运算符。如我们所知,在默认情况下排序算法使用 operator< 将序列按照升序排序。如果要传入降序,我们可以传入一个 greater 类型的对象。该类产生一个调用运算符并负责执行待排序类型的大于运算。例如,如果 svec 是一个 vector<string>,

// 传入一个临时的函数对象用于执行两个 string 对象的 > 比较运算。
sort(svec.begin(),svec.end(),greater<string>());

​ 需要特别注意的是,标准库规定其函数对象对于指针同样适用。我们知道,比较两个无关指针将产生未定义行为,然而我们可能希望通过比较指针的内存地址来 sort 指针的 vector。直接这么做将产生未定义行为,因此我们可以使用一个标准库函数对象来实现该目的:

vector<string*> nameTable;		// 指针的 vector

// 错误,nameTable 中的指针彼此之间没有关系哦,< 将产生未定义行为
sort(nameTable.begin(),nameTable.end(),
    [](string *a,string *b) { return a < b; });

// 正确,标准库规定指针的 less 是定义良好的
sort(nameTable.begin(),nameTable.end(),less<string*>());
练习 14.42
  1. 统计大于 1024 的值有多少个:

    auto s = count_if(ivec.begin(),ivec.end(),bind(greater<int>(),placeholders::_1,1024));
    
  2. 找到第一个不等于 pooh 的字符串

    auto x = find_if(svec.begin(),svec.end(),bind(not_equal_to<string>(),_1,"pooh"));
    
  3. 将所有值乘以 2

    transform(vec.begin(),vec.end(),vec.begin(),bind(multiplies<int>(),_1,2));
    

    需要注意:不能用 for_each,虽然 for_each 能对每个数都 *2,但是将值并不会改变到原对象上。

可调用对象与 function

​ C++有几种可调用对象:函数、函数指针、lambda 表达式、bind 创建的对象以及重载了调用运算符的类。

​ 和其他对象一样,可调用的对象也有类型。例如,每个 lambda 有它唯一的 (未命名) 类类型;函数及函数指针的类型则由其返回值类型和实参类型决定。

​ 然而,两个不同类型的可调用对象却可能共享同一种调用形式调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:

int(int, int)

是一个函数类型,它接受两个 int,返回一个 int。

不同类型可能具有相同的调用形式

​ 对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。例如,考虑下列不同类型的可调用对象:

// 普通函数
int add(int i,int j) { return i + j; }
// lambda,其产生一个未命名的函数对象类
auto mod = [](int i,int j) { return i % j; }
// 函数对象类
struct divide {
    int operator()(int denominator,int divisor) {
        return denominator / divisor;
    }
}

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

int(int, int)

​ 我们可能希望这些课调用对象构造一个简单的桌面计算器。为了实现这一目的,需要定义一个函数表用于存储指向这些可调用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。

​ 在 C++ 中,函数表很容易通过 map 来实现。对于此例,我们可以使用一个表示运算符符号的 string 对象作为关键字;使用实现运算符的函数作为值。当我们需要求给定运算符的值时,先通过运算符索引 map,然后调用找到的那个元素。

​ 假定所有的函数相互独立,并且只处理二元运算,则 map 可以定义为以下形式:

map<string, int(*)(int, int)> binops;		// 需要知道,这里的 second 是函数指针,不是可调用对象

我们可以按照以下形式将 add 指针添加到 binops 中:

binops.insert({"+", add});

但是我们不能将 mod 或 divide 存入 binops:

binops.insert({"%", mod});		// 错误,mod 不是函数指针

我们知道 mod 是一个 lambda 表达式,而每个 lambda 有它自己的类型,该类型与存储在 binops 中的值的类型不匹配。

标准库 function 类型

​ 我们可以使用一个名为 function 的新的标准库类型解决上述问题。function 在头文件 functional 中,下面是 function 定义的操作:

function<T> f;		// f 是一个用来存储可调用对象的空 function,这些可调用对象的调用
								  // 形式应该与函数类型 T 相同。

function<T> f(nullptr);		// 显示构造一个空 function

function<T> f(obj);				// 在 f 中存储可调用对象 obj 的副本

// **定义为 function<T> 的成员的类型**
result_type;			// 该 function 类型的可调用对象返回的类型

argument_type;	 				   // 当 T 有一个或两个实参时定义的类型。如果 T 只有一个实参, 则 argument_type
first_argument_type;		  // 是该类型的同义词;如果 T 有两个实参,则用 first_argument_type 和
second_argument_type;	// second_argument_type 分别表示两个实参的类型

function 是一个模板,我们需要在定义时提供能够表示该对象的调用形式。如:

function<int(int, int)>

这里我们声明了一个 function 类型,它可以表示接受两个 int、返回一个 int 的可调用对象。所以我们可以这样利用 function 来完成上述的 map 函数表:

function<int(int, int)> f1 = add;					// 函数指针
function<int(int, int)> f2 = divide();			 // 函数对象类的对象
function<int(int, int)> f3 = [](int i,int j) { return i * j};		// lambda

map<string, function<int(int, int)>> binops;
// 然后 insert
binops.insert({"+", f1});
binops.insert({"/", f2});
binops.insert({"*", f3});

// 当然,也可以直接在定义时就初始化
map<string. function<int(int, int)>> binops = {
    {"+", add},
    {"*", [](int i,int j) { return i * j; }},
    {"/", divide()}
}

所以,我们便可以这样来调用这些函数:

binops["+"](10, 5);			// 15
binops["/"](10, 5);			// 2
重载函数与 function

​ 我们不能直接将重载函数的名字存入 function 类型的对象中:

int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add});			// 错误,add 是指哪一个 add

解决上述二义性的一个方法是用 函数指针:

int (*fp)(int, int) = add;
binops.insert({"+", fp});

同样,也可以用 lambda:

binops.insert({"+", [](int i,int j) { add(i, j); }});
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值