c/c++进阶之爱恨交织的临时对象:三、诡异的性质

在通过前两章的讲解之后,相信大部分读者都能写出比较优雅并高效的的程序了。但是,因为各家厂商编译器实现和语言本身的问题(c++语言本身就是让人又爱又恨的),我们在编写一些跨平台程序的时候,冷不防的就会踩坑趟雷,苦不堪言。(不要问我为什么知道,因为我的眼角都是泪水)

对于临时对象,我们还有以下三点要考虑:

  1. 它是一个常量对象么?
  2. 它被引用绑定么?
  3. 当函数按引用传参时,实参为临时对象会怎么样?

首先大家可以稍稍思考下这几个问题。然后,我来给大家一一解答。

1、临时对象会自动地的成为常量对象么?

这是一个令人困惑的问题。这种困惑其实是来自于内置类型的临时对象是不可修改并且不可赋值而引起的。来看下面的代码:

int func()
{
    int data(5);
    return data;
}
int main()
{
    func() = 4; //ERROR
    func()++;   //ERROR
}

有的人会说,从上述代码就可以看出来临时对象是自动具有常量性的,不可修改并且不能被赋值么。相信,有如此观点的人不会少,甚至包括大名鼎鼎的《c++编程思想第一卷》《Thinking in CPP vol 1》也是如此。

Sometimes, during the evaluation of an expression, the compiler must create temporary objects. These are objects like any other: they require storage and they must be constructed and destroyed. The difference is that you never see them – the compiler is responsiblefor deciding that they’re needed and the details of their existence. But there is one thing about temporaries: they’re automatically const. Because you usually won’t be able to get your hands on atemporary object, telling it to do something that will change thattemporary is almost certainly a mistake because you won’t be able to use that information. By making all temporaries automatically const, the compiler informs you when you make that mistake.

上面这段话您可以在《Thinking in CPP vol 1》中第368页找到,也可以在中文版《c++编程思想第一卷》第183页找到。我们将它的示例代码同样写在这里来做分析。

class X {
    int i;
public:
    X(int ii = 0);
    void modify();
}
X::X(int ii) {i = ii;}
void X::modify(){i++;}
X f5(){return X();}
const X f6(){ return X();}
void f7(X &x){x.modify();}
int main() {
    f5() = X(1); // OK -- non-const return value
    f5().modify();// OK
//  Causes compile-time errors:
//! f7(f5());
//! f6() = X(1);
//! f6().modify();
//! f7(f6()); 
}/// :~

作者在上面的一段话中解释为什么f7(f5())这个函数无法通过编译的原因是,因为f7(X &)接受一个非常量引用做参数,但是编译器要生成一个临时对象来保存f5()的返回值,然后传递给f7()函数,但是由于临时对象自动成为常量,所以编译器通不过。

事实上,作者的观点即使是在他自己所写代码中都是自相矛盾的。好的,我们姑且认为自动成为常量是对的,那么f5().modify()为什么又是合法的呢?我们知道,一个常量对象是只允许调用const限定的成员函数的,但是modify()显然是non-const 成员函数的。

自相矛盾!对于临时对象自动成为常量对象显然是错误的。(再次验证古话,尽信书不如无书 :)

实际上,第一段代码内的func()无法被赋值和修改是因为func()是一个右值表达式(rvalue expression),也就是说临时对象是一个右值。c++标准规定对于右值:

  1. Address of an rvalue may not be taken: &int(),&i++,&42 and &std::move(x) are invalid.
  2. An rvalue can’t be used as the left-hand operand of the build-in assignment or compound assignmeng operators.

这就解释了为什么func()是无法被赋值和修改了,因为它不位于内置 “=”的左侧。至于为什么不能取右值的地址,个人猜测,是因为右值的内存空间位于寄存器内的,并且修改右值表达式的值通常是无意义的(因为它是临时的)。

那么对于自定义类型,“=”操作又可以呢?上面的代码中f5() = X(1)可确实是合法的。怎么理解呢?

这是因为 operator=() 实际上是一个成员函数。f5() = X(1)可以变成这种形式:

f5() = X(1) 等价于 f5().operator=(X(1)),这当然是合法的。

我们知道,C++的设计初衷是让用户自定义的类型(class)能像内置类型( int )一样工作,具有同等的地位。那么既然内置类型的临时对象不能做被赋值操作,那么我们可不可以让自定义类型的临时对象不能调用赋值函数呢?答案是可以的,但允许我卖个关子,等会再告诉你怎么做。:)

小结 : 临时对象不会自动地成为常量对象,但它是一个右值表达式。

2、临时对象能被引用绑定么?

临时对象通常是匿名的,看不见又摸不着,所以我们也没办法操作它,只能让它自生自灭。在纯c语言里面确实是这样的,但是c++中引入了引用这个概念,引用可以用来绑定一个变量,而成为这个变量的一个别称,从而操作这个引用就相当于操作变量本身。那么,我们可不可以用引用来绑定一个临时对象呢?

可以也不可以,具体来说就是我们不可以用非常量左值引用来绑定一个右值,但是可以用常量左值引用来绑定右值,也可以用右值引用来绑定右值。见下面代码:

int &ref = int(); //ERROR
const int &ref = int(); // OK
int &&ref = int();// OK

为什么非常量左值引用不能绑定临时对象呢?因为尽管临时对象不是常量对象,但对于内置类型来说,临时对象却是immutable的,是read-only的,但如果被非常量左值引用绑定之后,就有可能通过引用来改变临时对象的值了,那样的话仅可读的属性就变成可读写了,这当然是不对的。对于自定义类型,虽然我们可以通过成员函数来改变成员变量的值,但是为了与内置类型保持一致,C++标准还是规定,不能用右值来初始化非常量左值引用。

通过上面的分析就很容易知道为什么常量左值引用可以绑定右值了,因为它本身就是read-only的,不会改变上下文的语义。(tips:实际上常量左值引用是一个“万能”的引用类型,它可以接受非常量左值,常量左值,非常量右值,常量右值来进行初始化。)

而右值引用就是C++11中专门用来绑定右值的,关于右值引用,后面我会专门来讲,这里大家只要知道右值引用可以用右值来初始化就好了。

我们知道,临时对象的析构发生在最大表达式的结束处,临时对象就死亡了。但是如果被常量左值引用或者右值引用绑定之后呢?我们说,如果被成功绑定的话,那么临时对象就不会立即死亡,也是临时对象被“续命”了。在余生里面,可以通过引用来操作它们,但是对于常量左值引用而言,临时对象的余生是只可读的;而对于右值引用来说,它的余生是可读写的,与正常人无异啊。

读到这里,我想你大概明白引用与临时对象的关系了。但是有时候编译器的表现会让我们大跌眼镜。来看下面的简单代码。

#include <iostream>
#include <cstdlib>
using namespace std;
class CTest
{
public:
    CTest(){ cout << "call constructor" << endl; }
    ~CTest(){ cout << "call destructor" << endl; }
};
int main()
{
    int &data = int();//ERROR
    CTest &test = CTest();
    return 0;
}

当我在visual studio 2013编译的时候,编译器会告诉我: 非常量引用的初始值必须为左值。没错,这和我们上面的分析是一致的。
这里写图片描述

但是当我注释完这句话之后,对于自定义类型,用临时对象来初始化一个非常量左值引用却是可以的,而且从打印来看,还成功的续命了。
这里写图片描述

这与我们上面的分析就不一致了呀,与c++标准规定的都不一致呀 ? 怎么回事 ? 是我们理解错了?让我们先在g++编译器去试一下再来分析。

这里写图片描述

实际上,这一段代码在g++编译器下是无法通过编译的,这和我们的预期是一致的。

我没法解释为什么visual studio 2013对于自定义类型可以用非常量左值引用来绑定右值(因为如果可以的话,那就不必要引入右值引用了)。但是既然标准是规定不让绑定,那我们程序员在编写程序时就一定要避免写出这样的代码。不然就会出现在vs2013下运行的好好的,到linux下用g++编译都通不过的怪现象了。

三、当函数按引用传参时,实参为临时对象会怎么样?

在第一小节中,我们就知道了当函数形参为非常量左值引用类型时,实参为临时对象时是通过不了编译的。当然原因肯定不是在《c++编程思想第一卷》讲的那样,临时对象自动成为常量对象,然后balabala什么的。而是在第二节中我们讲过的,一个非常量左值引用的形参是不能用身为右值的实参来初始化的。

考虑下面这段代码:

#include <iostream>
#include <cstdlib>
using namespace std;
class CTest
{
public:
    CTest(){ cout << "call constructor" << endl; }
    ~CTest(){ cout << "call destructor" << endl; }
};
void func(CTest &){}
int main()
{
    func(CTest());
    return 0;
}

当然在g++编译器下是无法通过的:

这里写图片描述

但是对于vs2013来说:

这里写图片描述

相比较于直接用非常量左值引用去绑定一个右值,这种方式出错更隐蔽。所以在写程序的时候,一定要注意规范。

一个好的建议来自 Google C++ 编程规范

事实上这是一个硬性规定:输入参数为值或常数引用,输出参数为指针;输入参数可以是常数指针,但不能使用非常数引用形参

总结:通过这一章的学习,我们知道了临时对象的一些性质以及和引用的关系,并发现了其在不同编译器下拥有着不同的表现。所以为了编写出健壮的可移植的跨平台程序,我们一定要按照c++标准规范来,并可参照google c++ 编程规范其中的观点。

ps : 本人学识有限,欢迎各位大佬留言交流

参考资料
《Thinking in CPP vol 1》
《C++编程思想第一卷》中文版
《深入理解C++11(C++11新特性解析与应用)》
《Google C++ 编程规范》
https://accu.org/index.php/journals/227
http://www.cnblogs.com/BensonLaur/p/5234555.html

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值