for_each()函数、临时对象的产生与()运算符重载的意义

所谓临时对象,就是一种无名对象(unamed objects),有时候使用临时对象可以使程序干净清爽,比如可以在类型名称之后直接加一对小括号()或者大括号{}(列表初始化),并可指定初值,例如shape(3,5)或者int{8},其意义相当于调用相应的constructor并且不指定对象名称
for_each()是一种STL函数,存在于algorithm库中,它可以用于很多容器内,for_each(iterator,iterator,pointer)是一般形式,最后一个pointer是指向函数的指针(更普遍的是,最后一个参数是一个函数对象)。

using namespace std;
template<typename T>
class print
{
public:
	void operator()(const T& elem)
	{
		cout << elem << ' ';	
	}
};

template<typename T>
void showReview(const T& elem)
{
	cout << elem << ' ';
}

int main()
{
	int ia[6] = { 0,1,2,3,4,5 };
	vector<int>iv(ia, ia + 6);
	for_each(iv.begin(), iv.end(), print<int>());
	for_each(iv.begin(), iv.end(), showReview<int>);
}

如以上代码所示,我们初始化一个vector数组模板,并且将012345等6个数传入,使用for_each分别调用函数指针(函数名)和无对象名模板类实例,以上两种版本均可以起效。上面代码的print()是function template具体化的一个临时对象,这个对象被传入for_ezch()之中起作用,当for_each()结束时,这个临时对象也就结束了它的生命。
另外需要注意的是print()到底是调用了print模板类的默认构造函数创建了一个临时对象还是调用了()运算符函数呢?
按理说这个print类的构造函数并没有定义,只是调用了默认构造函数,那并不会有输出数组元素的功能,所以在这里应该是()运算符重载起到的效果,这意味着创建了一个将会在for_each()函数中调用重载()运算符函数的print临时对象,这种定义了函数operator()的类称为一种函数符或者函子(functor)。

那么为什么要用()运算符重载呢?

以下装载自知乎网友Holy Chen在问题C++小括号重载的意义是什么?下的回答
https://www.zhihu.com/question/336055609
常见的有两种用法,一种是Callable,一种是索引。
Callable
Callable,也就是可调用对象,包括了函数指针、重载operator()的对象以及可隐式转化为前两者的对象。重载operator()的对象,也称Functor,中文翻译有时候叫做函子。在谈论为什么使用Functor之前,我们先来看
看函子是什么,以及怎么用。
比如,我们这里有一个函数,叫做for_each,是std::for_each的简化版,它对于C数组中的每一个元素都进行
一个处理。在函数中,Func类型定义了一个Callable的参数。

template <typename T, typename Func>
void for_each(T* begin, T* end, const Func& f)
{
 while (begin != end) f(*begin++);
}

现在我们定义一个函数print,它的功能是打印一个变量。我们把它作为函数for_each的第三个参数,以打印
一个int数组。

template <typename T>
void print(const T& x)
{
 std::cout << x << " ";
}
int main()
{
 int arr[5] = { 1, 2, 3, 4, 5 };
 // 这里的print<int>自动decay为decltype(&print<int>)
 for_each(arr, arr + 5, print<int>);
 return 0;
}
我们可以再写一个Functor的版本。
template <typename T>
struct Print
{
 void operator()(const T& x) const
 {
 std::cout << x << " ";
 }
};
for_each(arr, arr + 5, Print<int>{});

当然,如果你想要通过写一份难以理解但能用的代码来阻止其他人维护(可能为了报复不让你午休的CEO),
你也可以定义一个能够转化为函数指针的类型,这就是Callable的第三种形态。应该注意到,这里本质上还是
一个函数指针。

struct PrintForYuTangCEO
{
 typedef void(*Func_Type)(const int&);
 operator Func_Type() const { return &print<int>; }
};
for_each(arr, arr + 5, PrintForYuTangCEO{});

现在,你可能突然有了一个奇怪的需求,想要对打印的对象进行计数,你当然可以在函数print中加入一个
static变量,但是这破坏了面向对象,也不利于使用(报复CEO除外)。

static int count = 0;
template <typename T>
void print(const T& x)
{
 std::cout << count << " : " << x << std::endl;
 count++;
}
count = 0;
for_each(arr, arr + 5, print<int>);

如果使用Functor来定义,则会方便得多。

template <typename T>
struct Print
{
 mutable int count = 0;
 void operator()(const T& x) const
 {
 std::cout << count << " : " << x << std::endl;
 count++;
 }
};
for_each(arr, arr + 5, Print<int>{});

当然,你也可以不重载operator(),而是采用一个普通的成员函数来print。但是这会让你的代码写的比较
exciting!当然,这种用法在某些框架中随处可见,主要用于传入回调函数。

template <typename T, typename Func, typename... Args>
void for_each_Ex(T* begin, T* end, const Func& f, const Args&... args)
{
 while (begin != end) std::invoke(f, args..., *begin++);
}
template <typename T>
struct Print_Exciting
{
 mutable int count = 0;
 void print(const T& x) const
 {
 std::cout << count << " : " << x << std::endl;
 count++;
 }
};
for_each_Ex(arr, arr + 5, &Print_Exciting<int>::print, Print_Exciting<int>{});

这里有人会问,std::invoke是C++17才出现的函数,哪怕是变参模板也是C++11的语法,那么此前将类中普
通成员函数作为参数该怎么写呢?这里就要涉及一个more exciting的写法了,成员函数指针及其调用。
// 这里C是Func函数所属的类别,这里的函数f是类C中除类限定外类型为Func的成员函数.

// 具体于本例,T = int, Func = void(const int&) const, C = Print_Exciting<int>
template <typename T, typename Func, typename C>
void for_each_More_Ex(T* begin, T* end, Func C::* f, const C& obj)
{
 while (begin != end) (obj.*(f))(*begin++);
}
// 应

该注意到,for_each函数的调用方式没有任何变化
for_each_More_Ex(arr, arr + 5, &Print_Exciting::print, Print_Exciting{});
值得一提的是,由于我们只能够在定义形参时指定函数类型,而非带有函数名的签名,因此这两种普通成员函
数指针做参数的写法,并不能够提供充足的信息以供编译器优化,因为你完全可以在同一个类中提供两个甚至
更多具有相同类型,但名字不同函数,这使得函数的地址是运行时的,而非编译时的。这对于普通的函数指针
同样成立。
而operator()在给定签名后,是可以在编译期唯一确定的,因此编译器可以对operator()的调用做出优化,例
如将其内联。
当然,你也可以将函数指针作为模板的非类型模板参数而不是函数的形参,这使得函数指针成为了编译时常
量,因此可允许编译器优化。但是由于非类型模板参数中的类型名只能引用模板形参列表中前面出现过的类型
名,因此会你的函数写法极其exciting,需要手动指定模板实参以实例化(专用化),使用时极不方便,在语
法上是一个灾难。例子如下。

template <typename Func, typename C, Func C::* f, typename T>
void for_each_More_More_Ex(T* begin, T* end, const C& obj)
{
 while (begin != end) (obj.*(f))(*begin++);
}
// 务必注意第一个函数类型中不可出现(*),如果出现会导致模板具体化时类型错误,
// 即f会成为一个诡异的类型,如果第一个实参为void (*)(const int&),则f为
// void (* Print_Exciting<int>::* )(const int&) const,无法实例化该模板
for_each_More_More_Ex<void(const int&) const, Print_Exciting<int>, &Print_Exciting<int>::print>
 (arr, arr + 5, Print_Exciting<int>{})

;
此外,Functor还有一个比较有趣的用法,是Functor重载。比如,你可以用同一个Functor来同时实现Hash
和Equal,这会让你的类变得好用一点(也许吧)。

struct A
{
 int x;
};
struct A_Hash
{
 std::size_t operator() (const A& a) const
 { return std::hash<int>{}(a.x); }
};
struct A_Equal
{
 std::size_t operator() (const A& lhs, const A& rhs) const
 { return lhs.x == rhs.x; }
};
template <typename... Ops>
struct AllOps : Ops...
{
 using Ops::operator()...;
};
using A_Hasher = AllOps<A_Hash, A_Equal>;
std::unordered_set<A, A_Hasher, A_Hasher> a_hashset;

举了这些例子之后,你现在可能已经理解了一些Functor的好处。
第一,Functor是C++风格的Callable,C++标准对Functor的支持要更为完善,提供了丰富的接口以及编译
器优化,比如Functor会被编译器优化为内联函数。
第二,Functor可以方便的进行有状态操作。
第三,Functor是面向对象的,让你的代码更为抽象。
第四,你可以同时将多个函数聚集在同一个Functor上,实现重载。
索引
如果你用过Python,你一定会嫉妒numpy中的index,你可以用方括号的方式来索引多维数组。比如

import numpy as np
x = np.random.randn(10, 10)
x[5, 5] # correct

不幸的是,C++的operator[]只允许你声明一个参数,这就导致这个运算符对于多维数组而言无比鸡肋,然而
这种需求对于矩阵等类型而言又是客观存在的。所以,你可能会见到一些库使用operator()实现这个操作。这
里,我们就举一个矩阵的例子。

template <typename T, std::size_t ROWS, std::size_t COLS>
struct Matrix
{
 T data[ROWS][COLS];
 T operator() (int x, int y) const
 {
 return data[x][y];
 }
 T& operator() (int x, int y)
 {
 return data[x][y];
 }
 template <typename... Args>
 auto get(Args&&... args) const
 { return this->operator()(std::forward<Args>(args)...); };
};
Matrix<int, 10, 20> m;
m(5, 5) = 10;
std::cout << m.get(5, 5) << std::endl;

总之,C++中operator ()的重载主要用于实现Functor和索引,前者是对函数指针的面向对象化,具有诸多优点,后
者则是为了弥补operator[]不能使用两个参数。

总结:

for_each()函数的最后一个实参可以是函数指针(函数名),也可以是一个临时对象,其意义相当于直接调用了类的构造函数但并不指定对象名称。因为函数不能直接作为其他函数的参数或返回值,而 callable 的对象和函数指针则可以,我们将重载 了operator() 的类的对象以及函数指针和函数名都称为函数对象或函子。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值