最近写程序的时候突然意识到了一个(也许大多数人早就意识到的)很常见的问题。我们知道复制一个对象(尤其是复杂的对象/实例)往往需要不小的计算开销(更别提复制一个数组的对象了)。但另一方面,复杂对象(群)的建立往往需要单独写一个函数来处理。那么如何从函数中输出这些对象?
或者看下面这个例子。
class object{
...
};
object createObject( parameter ){
object s( parameter );
return s;
};
int main(){
while(...){
vectors.push_back(createObject( parameter ));
}
}
如果我们想在createObject
函数内根据(可能动态改变的)参数parameter
来构造对象,并将新建的对象填入数组中。再毫无优化的前提下,(取决于createObject
的返回值类型)对象可能需要进行2次的复制构造。createObject
将s
复制给一个临时对象作为返回值,然后该返回值在被push_back
进vector
时再次被复制。即使我们根本不想要那么多复制出来的对象,WTF。
经验告诉我们,直接创建的对象都是保存在栈内存中的。当离开其作用域(无论因为异常还是return
),堆内存会释放,这个对象也不复存在了(即使可能内存上的值不会立刻被覆盖刷新)。
一个简单的办法是将对象建立在堆上(用new
关键字),并返回它的指针。但这意味着程序员必须显式地在另外的函数中对该指针调用delete
方法来释放该内存避免泄漏。这很不安全,尤其在团队合作中,你的代码可能需要被另外的程序员调用。
顺着这个思路,智能指针是解决这个问题比较安全的方法。但作为一个c++程序员(而不是c程序员),总觉得不在最必要的时候使用指针显得不那么elegent(即使内部机理大同小异)。
所以说白了,我期待的是一个能建立在栈内存上,并且可以作为函数返回值(释放栈内存后仍然有效)的办法。
createObject
函数内无非是进行一些构造对象的操作,对象的使用价值还是在外面体现的。所以如果只是在函数内创建的临时对象如果只是函数离开后就析构就太浪费了。我本来在想能否通过C++11提供的右值引用和移动语义来获得函数的返回值所有权。
在Stack Overflow提问这个问题的时候,偶然得知了Copy Elision这个玩意。原贴在这。另外,这是个更直接的问题,以及一篇reference。
总结一下大概可以理解为,编译器发现程序员就是单纯的需要一个对象为不是其复制时,有可能会在编译阶段优化掉(也就是省略掉)这些不必要的复制。具体可以看答案下的网友给的回复(这里是复制过来的):
#include <iostream>
#include <string>
#include <vector>
using Params = std::vector<std::string>;
class Object
{
public:
Object() = default;
Object(const std::string& s) : s_{s}
{
std::cout << "ctor: " << s_ << std::endl;
}
~Object()
{
std::cout << "dtor: " << s_ << std::endl;
}
// Explicitly no copy constructor!
Object(const Object& other) = delete;
Object(Object&& other)
{
std::swap(s_, other.s_);
std::cout << "move: traded '" << s_ << "' for '" << other.s_ << "'" << std::endl;
}
Object& operator=(Object other)
{
std::swap(s_, other.s_);
std::cout << "assign: " << s_ << std::endl;
return *this;
}
private:
std::string s_;
};
using Objects = std::vector<Object>;
Object createObject(const std::string& s)
{
Object o{s};
return o;
}
int main ()
{
Objects v;
v.reserve(4); // avoid moves, if initial capacity is too small
std::cout << "capacity(v): " << v.capacity() << std::endl;
Params ps = { "a", "bb", "ccc", "dddd" };
for (auto p : ps) {
v.push_back(createObject(p));
}
return 0;
}
结果如下
capacity(v): 4
ctor: a
move: traded ‘a’ for ”
dtor:
ctor: bb
move: traded ‘bb’ for ”
dtor:
ctor: ccc
move: traded ‘ccc’ for ”
dtor:
ctor: dddd
move: traded ‘dddd’ for ”
dtor:
dtor: a
dtor: bb
dtor: ccc
dtor: dddd
可以发现,即便显式地禁止了复制构造函数,但该段代码仍然可以运行(虽然这里调用了移动构造函数)。我尝试了下反过来,禁用移动构造函数而启用复制构造函数,结果也是只调用了一次复制构造函数。
GCC的编译选项-fno-elide-constructors可以主动关闭复制省略。
当然了,这并不是最理想的结果,因为我们还是对每个对象进行了一次没有必要的复制/移动(也就是其实建立了两倍数目的对象),但至少也减少了一次了不少么?
–如果我后面找到了可以一次复制/移动也不需要的方法会继续补充。