標籤:

C++函數返回值拷貝問題?

我有一個函數

vector& Get()
{
vector& temp;
//fill temp with some values

return temp;
}

每次我調用它的時候都是這樣的:

vector& vec = Get();

但是這樣的話函數返回的時候會把temp里的元素都複製一遍,我覺得這樣效率很低,好不容易建立了一個temp的vector,結果還得把它複製一遍,感覺效率太低了,不知道各位大大是怎麼處理這種情況的?

我目前能想到幾種方法:

1. 在調用函數之前建一個vector,然後把這個vector用引用的方式傳給Get函數:

void Get(vector& vec);

2. 在Get里new一個vector對象,返回指針:

vector&* Get();

但是這個方法感覺很醜,delete的時候也得在外層函數處理。

想到這個問題的契機是STL的substr方法。據我所知,STL里的substr方法返回的就是一個臨時的string,所以每次調用substr的時候substr會新建一個字元串,然後返回該字元串的時候會複製一遍,這樣效率不會很低嗎?


謝邀,這不就是C++11右值引用和Move語意要解決的問題嗎? Ref: Rvalue References and Move Semantics in C++11

就 @Milo Yip提的疑問,我認為編譯器會選擇RVO/NRVO,因為觸發了C++11 Copy Elision的條件,標準12.8節如下:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function』s return value
  • [...] // Exception
  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
  • [...] // Exception

簡單的測試:

#include &
#include &

struct X
{
X() {std::cout &<&< "constructed "; } X(const X) { std::cout &<&< "copied "; } X(X) { std::cout &<&< "moved "; } ~X() {std::cout &<&< "destructed "; } }; std::vector& Get()
{
// copy elision from tempory object to "temp", omit copy and move constructor
std::vector& temp = std::vector&(1);

return temp; // NRVO temp to returned
}

int main()
{
std::vector& v = Get();
}

// Output

/*
constructed
destructed
*/

這個問題我覺得可以深挖下去,我準備和同事一起就這個繼續討論一下,然後寫一篇博客在C/C++ Cafe裡面,後面寫完博文後,我把地址貼出來,然後補上,留個坑在這裡。

================2015年7月17日更新===========

博文地址:RVO V.S. std::move (C/C++ Cafe)

==============2015年7月20日更新======

isocpp地址:https://isocpp.org/blog/2015/07/rvo-v.s.-stdmove-zhao-wu


編譯器能自動進行返回值優化(Return value optimization),避免拷貝。

在C++11中,除了RVO/NRVO,非局部對像可以使用return std::move(x)的方式去避免拷貝。


把你的編譯器升級到VS2010或以上,然後就可以放心大膽的這樣寫而且也不會引起多餘的複製了。


Visual Studio 2015已支持自動生成默認Move Constructor/Move Assignment Operator,所以可以更加肆無忌憚地直接返回了。


「我目前能想到幾種方法:

1. 在調用函數之前建一個vector,然後把這個vector用引用的方式傳給Get函數:

void Get(vector& vec);」

你的第一個方法是恰當的。內存管理全部由調用方負責。此函數不關心參數的生命周期。這是 c++ 的分工原則。

你的方法(2),是這個函數 new 一個對象,然後由調用方 delete。這種方法要求調用方必須明確的知曉他具有 delete 的職責。這樣就不太好。

因為 c++ 是程序員負責內存管理,所以對這種分工的原則定義導致這樣的寫法。在 c# 這樣的語言中,因為程序員不關心內存管理,運行時跟蹤所有堆上的對象,然後自動釋放。因此就會發現普遍在c#中的常見寫法更多的等價於採用了你提到的方法(2)。


編譯器又不是不會做NRVO。。就算不會,move ctor也等著呢。

就算不行也不要用裸指針,std::unique_ptr大法好。


寫一個適合初學者版本的:

vector vec = Get();

vec會成為一個右值引用,持有Get的返回值,不會把它的返回值再做一遍拷貝的。


搞錯了,忽略以下答案

最簡單的方法好像是把Get改成vector& Get(),這樣編譯器會調用vector的move constructor:

vector (vector x);


這個就是C++醜陋的一面,RVO是一個不100%保證、不確定的編譯器行為

類似的醜陋行為還有用所謂的「智能指針」給所有堆上對象包一個棧上對象(而且一樣要考慮RVO)

如果是一般的編程語言也就算了,偏偏C++的用戶是一個非常注重代碼性能可預測性的群體。如果是強迫症的話,往往就不指望RVO而直接用指針傳遞數據(這樣有最大的性能可預測性),然後寫到一半覺得「那還用C++幹什麼呢?還是用C算了」


//這樣寫不是也挺好嘛,也不用new啊
void Get(vector& vec)
{
vec.push_back(1);
}


測試了下RVO和NRVO在分別在copy/move construct,copy/move assignment八種簡單情況,測試條件是g++ 4.8.2和clang++ 3.4,默認優化。

#include &
#include &
#include &

struct Test {
Test()
{
std::cout &<&< "construct a Test object" &<&< std::endl; } Test(const Test) { std::cout &<&< "copy construct a Test object" &<&< std::endl; } Test operator=(const Test) { std::cout &<&< "copy assignment a Test object" &<&< std::endl; return *this; } Test(Test) { std::cout &<&< "move construct a Test object" &<&< std::endl; } Test operator=(Test t) { std::cout &<&< "move assignment a Test object" &<&< std::endl; return *this; } ~Test() { std::cout &<&< "destruct a Test object" &<&< std::endl; } }; Test getTest() { return Test(); } Test getTestWithName() { Test temp; return temp; } int main() { std::cout &<&< "=============RVO==============" &<&< std::endl; std::cout &<&< "++Test obj rvo for copy construct" &<&< std::endl; auto obj1 = getTest(); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test obj rvo for move construct" &<&< std::endl; auto obj111 = std::move(getTest()); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test obj rvo for copy assignment" &<&< std::endl; Test obj11; obj11 = getTest(); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test object rvo for move assignment" &<&< std::endl; Test obj1111; obj1111 = std::move(getTest()); std::cout &<&< "=============NRVO==============" &<&< std::endl; std::cout &<&< "++Test obj nrvo for copy construct" &<&< std::endl; auto obj2 = getTestWithName(); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test obj nrvo for move construct" &<&< std::endl; auto obj222 = std::move(getTestWithName()); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test obj nrvo for copy assignment" &<&< std::endl; Test obj22; obj22 = getTestWithName(); std::cout &<&< "--------------" &<&< std::endl; std::cout &<&< "++Test obj nrvo for move assignment" &<&< std::endl; Test obj2222; obj2222 = std::move(getTestWithName()); std::cout &<&< "==============================" &<&< std::endl; return 0; }

結果輸出:

=============RVO==============
++Test obj rvo for copy construct
construct a Test object
--------------
++Test obj rvo for move construct
construct a Test object
move construct a Test object
destruct a Test object
--------------
++Test obj rvo for copy assignment
construct a Test object
construct a Test object
move assignment a Test object
destruct a Test object
--------------
++Test object rvo for move assignment
construct a Test object
construct a Test object
move assignment a Test object
destruct a Test object
=============NRVO==============
++Test obj nrvo for copy construct
construct a Test object
--------------
++Test obj nrvo for move construct
construct a Test object
move construct a Test object
destruct a Test object
--------------
++Test obj nrvo for copy assignment
construct a Test object
construct a Test object
move assignment a Test object
destruct a Test object
--------------
++Test obj nrvo for move assignment
construct a Test object
construct a Test object
move assignment a Test object
destruct a Test object
==============================
destruct a Test object
destruct a Test object
destruct a Test object
destruct a Test object
destruct a Test object
destruct a Test object
destruct a Test object
destruct a Test object

(另外,如果木有move assignment,會調用copy assignment,測試結果就不貼了,感興趣的可以自己改改看看move語義的影響)


return最好用好,理解清楚 ,不然坑很大,比如下面這段代碼。

#include&

class Man
{
public:
Man(){data =new int(0);}
~Man(){delete data;}
int* data;
};
Man get(Man m)
{
return m;
}

int main(int argc, char *argv[])
{

Man m,n;
*m.data = 5;
printf("m.data is %d
",*m.data);
n = get(m);
printf("m.data is %d
",*m.data);
printf("n.data is %d
",*n.data);

while(1);
}


vector& Get()
{
vector& temp;
//fill temp with some values

return temp;
}

我怎麼覺得你不可能獲得 temp 呢。。。

難道 temp 不是創建在棧上, } 一到就立刻被銷毀了嗎?你確定 C++ 會把 temp copy 一次?


請看c++ primer 5th


推薦閱讀:

operator=重載時,是否可以用一個按值傳遞版本取代按const引用傳遞和右值引用傳遞?
關於C++右值及std::move()的疑問?
C++11 中 typedef 和 using 有什麼區別?
C++中如下的變數聲明見過嗎?
auto recommended = 200"000U;

C++的class與struct到底有什麼不同?

TAG:編程 | C | CC | C11 |