漫谈pair

引子

故事的开端,是一位朋友将老代码迁移到C++11标准时遭遇了编译错误。他的代码大致如下:

1
2
3
4
5
extern void fun(std::pair<int ,int>);

int v = 5;

fun(std::make_pair<int,int>(v,6));

编译器抛出了错误:

cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’

错误的原因显而易见,std::make_pair<int,int>需要int &&作为参数,而v是一个左值,无法匹配。一个简单的解决方案是:

1
fun(std::make_pair<int&,int>(v,6));   //用int&替代int

这样写确实可以通过编译,fun函数也确实接收了一个pair<int ,int>对象,没有bug,可正常工作。不过,这背后牵涉的std::decay引用折叠等知识,并不是本文的重点,这里就不详细展开了。

聊聊pair

以上是个引子,基于这个引子,本文主要想聊聊std::pair<int, int>
std::pair<int, int>是个模板类,属于stl的一部分,它拥有两个数据成员firstsecond,并且提供了各种行为良好的构造函数,以及默认的<,==>等操作符。

让我们通过两个代码片段来比较一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// code segment A
stuct PersonInfo
{
int age;
int salary;
}
PersonInfo ZhangSan; //张三的信息
ZhangSan.age = 30;
ZhangSan.salary = 3000;

// code segment B
std::pair<int, int> LiSi; //李四的信息
LiSi.first = 40; //李四的年龄
LiSi.second = 4000; //李四的月薪

显然,代码A比代码B要易理解,agesalary变量名精确地表示了自身的含义,无需额外注释。因此,大多数情况下,我们不建议大家用pair来替代包含两个数据成员的结构体。

那么,pair应该在什么场合使用呢?

  • 第一种情况是,我们使用了第三方代码时,接口要求我们使用pair,例如在使用各种stl模块时。
  • 第二种情况是,我们编写代码时,还不知道这个结构体将用来存储什么样的具体数据,例如,当我们编写自己的容器模板代码时。
  • 第三种情况是,我们想对结构体进行“比较大小”的操作,但又不想自己重载对应的操作符。

第一种情况下,我们不得不用pair;第二种情况下,我们应该主动选用pair;而在第三种情况下,则应尽量避免使用,除非我们确信代码的混乱程度在可接受的范围内。

从pair到make_pair

回到引子的问题吧,那个编译问题是由make_pair函数引起的。

让我们来看看make_pair的引入。

考虑下面的代码(假设fun是一个接收特定pair参数的函数):

1
2
vector<int> vi;
fun(pair<vector<int>::const_iterator,vector<int>::const_iterator> (vi.cbegin(),vi.cend()));

这段代码中<vector<int>::const_iterator,vector<int>::const_iterator>的出现让人觉得冗长,且掩盖了重要信息。程序员们希望代码能更简洁,于是make_pair应运而生,代码可以写成:

1
2
vector<int> vi;
fun(make_pair(vi.cbegin(),vi.cend()));

相比之前冗长的版本,修改后的代码更容易让人抓到重点,fun函数接收了一个pair参数,其两个元素分别是vi.cbegin()vi.cend()

make_pair的演化

让我们来看看make_pair的演化历程。

最早,即上个世纪C++刚刚标准化时,make_pair的接口形式是:

1
2
3
template<class T1, class T2>

pair<T1, T2> make_pair(const T1 &x, const T2 &y);

参数类型是const &,这样可以避免不必要的拷贝。

然而,这样的定义存在问题。例如,make_pair("abc",3)这样的代码就会无法通过编译,因为T1类型会被推断成const char[4](而不是const char *),而pair<const char[4],int>是不合法的,因为first(T1)类型无法拷贝。

按我们的直觉,make_pair("abc",3)应该返回pair<const char * ,int>。我们的直觉重要吗?当然重要!因此,在本世纪初,C++03标准对make_pair的接口做了一个补丁,修改为:

1
2
3
template<class T1, class T2>

pair<T1, T2> make_pair(T1 &x, T2 &y);

这样,make_pair("abc",3)可以通过编译了,尽管理论上的代码执行效率有所降低。至于理论上的效率变低,标准还为此特意给编译器优化开了”绿灯“(这又是另外一个故事了,此处不再展开)。

随着时代的发展,程序员们对效率提出了更高的要求。于是,C++11带来了着右值引用等特性。

make_pair顺势而为,再次变更了接口,兼顾了“直觉”和效率,变成了:

1
2
template< class T1, class T2 >
pair</*V1*/, /*V2*/> make_pair( T1&& x, T2&& y );

这样,理论上大部分代码都更高效了。不过,极少数人可能会觉得困惑。还记得我们引子部分的代码吗?我再贴一次:

1
2
3
4
5
extern void fun(std::pair<int ,int>);

int v = 5;

fun(std::make_pair<int,int>(v,6));

这段代码在上个世纪是没有问题的,在C++11之前也没有没问题,但更新到C++11之后,就面临编译错误,就必须修改。

当然,这是代码本身的问题,要怪就只能怪程序员写得不好,不能怪标准的变化。具体原因我稍后再详细说明。

从make_pair回归pair

让我们回到最初引入make_pair的目的:简化代码,使其更易写易读。

例如让下面的代码变得更好读:

fun(pair<vector<int>::const_iterator,vector<int>::const_iterator> (vi.cbegin(),vi.cend());

当我们能使用C++17标准时,由于模板构造函数可以自动推导参数,我们不再需要借助make_pair,可以直接写成:

1
fun(pair(vi.cbegin(),vi.cend()));

这样,代码干扰更少,更清晰,也更接近自然语言的表达。

是的,当我们的编译器支持C++17标准时,make_pair可以逐渐退出历史舞台了。make_pair很好,但我们已经有了更好的替代方案。

关于代码的好坏

回到引子部分的代码,为什么我会说“要怪就只能怪程序员写得不好,不能怪标准的变化”呢?

因为写那段代码的人,他违背了make_pair被创造的初衷,make_pair的出现,本来就是为了减少一次模板参数的重复,而他却显式地写了一遍模板形参!

让我们对比一下,一个追求代码整洁有的程序员,哪怕在上个世纪,也会写出这样的代码:

1
2
3
4
5
6
extern void fun(std::pair<int ,int>);

int v = 5;

//fun(std::make_pair<int,int>(v,6)); //有代码品味的程序员,会本能地发现这个“脱裤子放屁”的味道
fun(std::make_pair(v,6)); //这才是简洁的代码,而且从c++98开始,直到20多年后的今天,都不会有编译问题

一个追求代码质量的人,即使不及时跟进语言标准的变化,也不会有太大问题。因为在大多数情况下,标准的进化会让原本就很好的代码运行得更快更好。

至于那些随着标准进化而无法编译的代码,通常它们本身就很糟糕。

语言标准的演进,本质上是在奖励优雅的代码(以及写代码的思路),而不是为糟糕的代码提供避风港。写出简洁、清晰、符合本意的代码,才是程序员应有的追求。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2024-2025 刘清
  • 访问人数: | 浏览次数: