文章目录
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数想比它们更加灵活。
下面名为 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
-
统计大于 1024 的值有多少个:
auto s = count_if(ivec.begin(),ivec.end(),bind(greater<int>(),placeholders::_1,1024));
-
找到第一个不等于 pooh 的字符串
auto x = find_if(svec.begin(),svec.end(),bind(not_equal_to<string>(),_1,"pooh"));
-
将所有值乘以 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); }});