C++不要让函数返回一个右值引用

本文探讨了《Effective Modern C++》中关于右值成员函数时,为何返回普通对象而非右值引用的原理。通过实例和错误演示,揭示了在不同场景下返回右值引用可能导致的问题,强调了正确捕获和使用右值的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在阅读《Effective Modern C++》的时候条款12在顺带介绍右值成员函数,给了一个类似这样的例子:

class Obj {
    int x_;
public:
    explicit Obj(int x) : x_(x) {}

    int getX() const {
        return x_;
    }

    void setX(int x) {
        x_ = x;
    }
};
class Test {
    Obj obj_;
public:
    explicit Test(int x) : obj_(x) {}

    Obj getObj()&& {
        return std::move(obj_);
    }
    Obj& getObj() & {
        return obj_;
    }
};

为了获取类Test的一个临时对象的Obj成员,我们定义了一个右值成员函数,返回一个Obj对象。当时看到这样的代码我脑中有个念头一闪而过?为什么一定要返回一个Obj对象而不是返回Obj&&右值引用呢?这样子不是更符合直觉吗?而且还少调用一次移动构造函数(如果外界也用一个右值引用进行接收的话)。

class Obj {
    int x_;
public:
    explicit Obj(int x) : x_(x) {}

    int getX() const {
        return x_;
    }

    void setX(int x) {
        x_ = x;
    }
};
class Test {
    Obj obj_;
public:
    explicit Test(int x) : obj_(x) {}

    Obj&& getObj()&& {	//返回一个右值引用
        return std::move(obj_);
    }
    Obj& getObj() & {
        return obj_;
    }
};

如果是一个函数返回一个右值引用的话似乎很容易看出来问题:

Obj&& test(int x) {
    return Obj(x);
}

这样显然会core dump,因为右值引用其实也不过是一个引用罢了,而这个引用指向的是一个已经返回的栈上对象,这个对象的内存会随着函数栈的销毁而销毁。

int main() {

    cout << test(10).getX() << endl;

    return 0;
}

运行程序结果:

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

可以看到程序果然已经崩溃。那么如果我们运行返回右值引用的右值成员函数会怎么样呢?

int main() {

    cout <<  Test(10).getObj().getX() << endl;			//输出10
    
    return 0;
}

程序正常运行,输出10。似乎我们可以得出这样一个结论:对于右值成员函数来说是可以返回右值引用的?等等,现在下结论似乎为时过早。上面普通函数返回一个右值引用导致程序崩溃是因为右值引用指向的内存空间已经被释放,而现在这个临时对象的内存是在当前函数栈上的,虽然对象已经析构,但是其指向的内存并没有失效,编译器也不会检查到这种在当前函数栈上的非法内存操作。如果我们手动的把这个临时对象的内存释放掉是不是就可以观察到程序的异常呢?
为了观察到这种现象,我们需要给Test对象在堆上分配内存,并且用一个右值引用保存这个Test对象的右值成员函数的返回值,然后再释放该Test对象,我们再次用这个右值引用去访问数据:

int main() {

    Test *t {new Test(10)};	//使用统一初始化
    cout << t->getObj().getX() << endl; //10
    Obj&& obj = std::move(*t).getObj();
    delete t;
    cout << obj.getX() << endl; //0

    return 0;
}

果然,我们观察到了异常,因为我们期待的是通过右值成员函数我们已经获取到了Test对象的Obj对象的右值引用,结果Test对象析构以后右值引用所指向的Obj失效了。这就和我们前面对普通函数返回一个右值引用的理解是一致的,如果一个函数返回的是一个右值,那么就应该在返回值阶段就用对象去捕获他,如果使用右值引用的话我们无法保证其所指向的空间是否有效(因为右值rvalue包含消亡值xvalue)。
对于lvalue,xvalue,prvalue,gvalue,rvalue的关系,可以看一下这篇博客:什么是 lvalue, rvalue, xvalue虽然我也没有看完

因此我们就可以得到一个结论:不要让函数返回右值引用,无论是普通函数(危害更大)还是成员函数,而应该返回一个普通对象去捕获右值

上面的demo中在释放内存前任何一个地方用普通对象去捕获右值引用都不会出现问题,因为会调用移动构造函数。

另一个稍微有意思的地方在于:如果一个函数返回一个右值引用,那么这个返回值就是一个右值引用的右值,那么他是一个什么值呢?联想到引用折叠的话,应该是一个右值引用。我们可以用下面的代码测试一下(使用《Effective Modern C++》里面提到的通过编译器显示推断类型的方法,条款4):

template<typename T>
class TD;

int main() {
	TD<decltype(Test(10).getObj())> rrObj;
}

编译器报错:

/home/edward/C++/code/Temp/main.cpp: In function ‘int main()’:
/home/edward/C++/code/Temp/main.cpp:39:37: error: aggregate ‘TD<Obj&&> rrObj’ has incomplete type and cannot be defined
39 | TD<decltype(Test(10).getObj())> rrObj;
| ^~~~~

我们可以看到的确是一个Obj对象的右值引用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值