返回右值?或者:为什么复制构造函数在"返回"时被调用<expression>?

returning rvalue? Or : Why does copy constructor get called on `return <expression>`?

本文关键字:返回 调用 lt gt expression 或者 复制 为什么 构造函数      更新时间:2023-10-16

我怀疑我模糊地知道我所观察到的原因,但我希望得到确认或纠正和一些解释。

我有以下代码:

template <class T>
class C
{
public:
    C () = default;
    C(const C& rhs) : mem(rhs.mem)
    {
        std::cerr << "copy" << "n";
    }
    // does not call copy constructor twice
    // friend C operator<<(C& src, unsigned shift)
    // {
    //     std::cerr << 1 << "n";
    //     C tmp(src);
    //     std::cerr << 2 << "n";
    //     tmp <<= shift;
    //     std::cerr << 5 << "n";
    //     return tmp;
    // }
    // does call copy constructor twice
    friend C operator<<(C& src, unsigned shift)
    {
        std::cerr << 1 << "n";
        C tmp(src);
        std::cerr << 2 << "n";
        return (tmp <<= shift);
    }
    friend C& operator<<=(C& src, unsigned shift)
    {
        std::cerr << 3 << "n";
        src.mem <<= shift;
        std::cerr << 4 << "n";
        return src;
    }
    T mem;
};
int main()
{
    C<int> c1;
    c1 << 3;
}

我有两个版本的C<T>::operator<<(C<T>, unsigned)

不同之处在于返回表达式的结果:

return (tmp <<= shift);

另一个返回一个变量:

return tmp

到目前为止,我认为这两个函数在语义上是相同的,带有return (tmp <<= shift);的函数只是更好的样式,如return a + 1将比int ret = a + 1; return ret更好的样式。显然并非如此,而且可能只适用于原子数据类型。return (tmp <<= shift);版本的输出如下所示:
1
copy
2
3
4
copy

另一个的输出如下:

1
copy
2
3
4
5

我的假设是正确的,事实上return (tmp <<= shift);在调用<<=后不返回tmp,而是在调用<<=后创建一个新对象作为tmp的副本?

按值返回时,必须初始化返回值。返回值是一个类型为C的对象。

return (tmp <<= shift);的情况下,这意味着(tmp << shift)C的初始化式。因为它是C类型的左值,所以这是一个复制构造。

有一个特殊的规则,对于形式为return identifier;的返回语句,即使identifier是左值,它也可以是移动构造。但是这个规则还不能扩展到其他表达式。

另一个版本的代码会激活特殊规则:

tmp <<= shift;
return tmp;

这里tmp可以被视为右值,使其可移动(这也使其成为一个复制省略上下文)。

在测试中,编译器实现了复制-省略。要在没有复制省略的情况下进行测试(如果您的编译器支持让用户配置),您还需要为C提供一个move构造函数。用户提供的复制构造函数抑制了移动构造函数的隐式生成。

operator <<=可能不会返回对输入参数的引用(可能会返回对另一个static/global变量的引用),因此编译器不能轻易决定使用返回值优化,而需要调用复制构造函数

在您的问题中:当您编写return (tmp <<= shift)时,编译器不知道(tmp <<= shift)是否会返回对tmp的引用,但如果您编写return tmp,编译器知道它并可以优化它。