是否返回对象
概述
《C++ 核心指南》的 F.20 这一条款:For “out” output values, prefer return values to output parameters
,即在函数输出数值时,尽量使用返回值而非输出参数。
之前的做法是调用者负责管理内存,接口负责生成,比如:
MyObj obj;
error_code = initialize(&obj);
这种做法和 C 是兼容的,一种略为 C++ 点的做法是使用引用代替指针。
如果对象有合理的析构函数的话,那这种做法的主要问题是啰嗦、难于组合。你需要写更多的代码行,使用更多的中间变量,也就更容易犯错误。
接口直接返回对象
一个用来返回的对象,通常应当是可移动构造 / 赋值的,一般也同时是可拷贝构造 / 赋值的。如果这样一个对象同时又可以默认构造,就称其为一个半正则(semiregular)的对象。我们应当尽量让我们的类满足半正则这个要求。
class matrix {
public:
// 普通构造
matrix(size_t rows, size_t cols);
// 半正则要求的构造
matrix();
matrix(const matrix&);
matrix(matrix&&);
// 半正则要求的赋值
matrix& operator=(const matrix&);
matrix& operator=(matrix&&);
};
在没有返回值优化的情况下 C++ 是怎样返回对象的。以矩阵乘法为例,代码应该像下面这样:
matrix operator*(const matrix& lhs, const matrix& rhs)
{
if (lhs.cols() != rhs.rows()) {
throw runtime_error("sizes mismatch");
}
matrix result(lhs.rows(), rhs.cols());
// 具体计算过程
return result;
}
注意对于一个本地变量,我们永远不应该返回其引用(或指针),不管是作为左值还是右值。从标准的角度,这会导致未定义行为(undefined behavior),从实际的角度,这样的对象一般放在栈上可以被调用者正常覆盖使用的部分,随便一个函数调用或变量定义就可能覆盖这个对象占据的内存。这还是这个对象的析构不做事情的情况:如果析构函数会释放内存或破坏数据的话,那你访问到的对象即使内存没有被覆盖,也早就不是有合法数据的对象了。
返回非引用类型的表达式结果是个纯右值(prvalue)。在执行 auto r = … 的时候,编译器会认为我们实际是在构造 matrix r(…),而…部分是一个纯右值。因此编译器会首先试图匹配 matrix(matrix&&),在没有时则试图匹配 matrix(const matrix&);即有移动支持时使用移动,没有移动支持时则拷贝。
返回值优化(消除拷贝)
看下面的测试代码(编译器版本gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project))
class Obj{
public:
Obj(){ cout<< "Obj" <<endl;}
~Obj(){ cout<< "~Obj" <<endl;}
Obj(const Obj&){ cout<< "const Obj&" <<endl;}
Obj(Obj&&){ cout<< "Obj&&" <<endl;}
};
Obj f()
{
return Obj();
}
int main()
{
auto obj = f();
return 0;
}
输出
Obj ~Obj
改动一下
Obj f()
{
Obj obj;
return obj;
}
GCC和Clang输出不变,MSVC 在非优化编译的情况下产生了不同的输出(优化编译使用命令行参数 /O1、/O2 或 /Ox则不变),即返回内容被移动构造了。
Obj
Obj&&
~Obj
~Obj
再改动一下
Obj f(bool f)
{
Obj obj1, obj2;
if(f) return obj1;
else return obj2;
}
输出
Obj Obj Obj&& Obj Obj ~Obj
如果删除Obj(Obj&&){ cout<< "Obj&&" <<endl;}
输出
Obj Obj const Obj& Obj Obj ~Obj
如果把拷贝构造函数也删除(注:此时是标成 = delete,而不是简单注释掉,注释掉的话编译器会默认提供拷贝构造和移动构造函数),在 C++14 及之前f()函数不能工作,但从 C++17 开始,即使对象不可拷贝、不可移动,对象仍然可以被返回的!C++17 要求这种情况,对象必须被直接构造在目标位置上,不经过任何拷贝或移动的步骤。
如果输出改为三元表达式
Obj f(bool f)
{
Obj obj1, obj2;
return f ? obj1 : obj2;
}
输出
Obj Obj const Obj& Obj Obj ~Obj
可见这种情况下三元表达式性能不如if-else语句。
NVO、NRVO和std::move
RVO即Return Value Optimization,是一种编译器优化技术,可以把通过函数返回创建的临时对象给去掉,可以达到少调用拷贝构造的操作。
NRVO,即Named Return Value Optimization。
class Obj
{
};
Obj f1() // RVO
{
return Obj();
}
Obj f2() // NRVO
{
Obj obj;
return obj;
}
std::move 会禁止 NRVO。
- 本文链接:https://morisa66.github.io/2021/02/28/ReturnObjOrNot/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。