標籤:

GCC編譯的程序為什麼沒有正確調用拷貝構造函數?

對於下面的程序,GCC好像並沒有按照標準在返回一個類的實例的時候,去調用對應的拷貝構造函數,作為比較,vs 2012的編譯器則是正確去調用了。

#include &
using namespace std;

class A{
public:
A(){cout&<&<"ctor"&<&

PS:有人說這是gcc把結果優化了,但是我記得CSAPP裡面說過編譯器優化的第一個前提是不能夠改變原來程序的運行結果,但是這個明顯有個輸出都沒有了,難道這也是優化而不是bug?


這叫【返回值優化】,是C++標準裡面特別指出來允許的。


Returning Rvalue References

You don』t have to and should not move() return values. According to the language rules, the standard
specifies that for the following code:

X foo()
{
X x;
//....
return x;
}

the following behavior is guaranteed:

  • If X has an accessible copy or move constructor, the compiler may choose to elide the copy. This
    is the so-called (named) return value optimization ((N)RVO), which was specified even before
    C++11 and is supported by most compilers.

  • Otherwise, if X has a move constructor, x is moved.

  • Otherwise, if X has a copy constructor, x is copied.

  • Otherwise, a compile-time error is emitted.

以上內容摘自 The C++ Standard Library 2nd

關於NRVO優化,可以參考:

Named Return Value Optimization in Visual C++ 2005


參考這一問題

X a = X();經歷了哪些過程?


如果你把 VS 的配置改成 Release,行為也和 gcc 一樣。

另一種情況,如果你的函數返回值不是一個可以在編譯時確定的變數,那麼這種優化是不會執行的。比如你一個函數有的分支返回 a,有的分支返回 b,那編譯器也不知道你到底返回誰,所以它也沒法幫你優化。


c++ 11有專門處理該問題的解決方案 叫 移動構造函數 如果類中有堆內存數據像例子中的操作的話 在沒有開啟優化的情況下 會調用移動構造函數減少拷貝構造函數的調用


附加樓主沒有提供gcc編譯-o參數的級別 也沒有告訴cl編譯器有沒有使用release配置


所以沒有對比的意義的 這些不必要的操作是可以被優化的

附加RVO(Return Value Optimization) in gcc and NRVO(Named Return Value Optimization) in clang can be disabled by -fon-elide-constructors, you can get the result below:


不是不調用,是沒必要調用,因為他就是在原地構建的對象。


剛好我最近也遇到了這個問題。最後加了--save-temps,看了編譯出來的彙編代碼,弄明白了gcc是怎麼搞的。以下的講解都是針對gcc的。因為這些都是實現上的細節,標準沒有規定,各個編譯器都不太一樣。

想要弄明白題主的問題,首先要弄明白下面這個問題。

*******GCC是如何將函數的返回值傳回來的?***********

對於原生類型,如int,short,double,指針等,作為返回值時,大多數編譯器都是寄存器傳參,準確一點,就是寄存器eax來傳遞返回值。這是導師告訴我的,我沒驗證過,你有興趣的話,可以去讀編譯出來的彙編碼。

但是,如果返回值是一個結構體,或者是一個類本身(以下都稱為結構體),這種傳遞返回值的方式就不行了,因為寄存器位數有限(比如現在的64位電腦, 一個寄存器也就能傳遞一個64位的數據),而結構體可以很大。就需要其它的方式來傳遞參數。通過看彙編碼,GCC是這麼乾的。凡是需要返回一個結構體的函數,調用這個函數的地方,會額外傳給這個函數一個地址。之後該函數就將返回值直接構造到這個地址那裡。這樣的好處在於,返回值直接就構造在了合適的地方,不需要進行額外的拷貝構造。算是一個非常巧妙的實現方式,避免了不必要的拷貝構造。給GCC點個贊!!

*********題主這個問題要稍微複雜一點*******

題主的getStudent()函數的返回值實現機制已經在上文介紹過了。但是,如果題主用這個返回值直接去構造一個新的變數,GCC就會做另外一個優化。他發現,你既然是用這個返回值去構造新的變數,那我直接就把這個新變數的地址傳給函數,讓函數的返回值直接構造在這個變數這裡就好了。

A aa = getStudent();
/*或者寫成這樣,體現這裡本應有個拷貝構造。
A aa(getStudent());
*/

具體拿題主給的這個例子來說,編譯器會直接把aa的地址交給getStudent()函數,之後getStudent()的返回值直接構造在了aa所在的內存位置。於是就完成了aa的構造,也沒有必要再去調用拷貝構造了。

*******如何評價這種實現方式*******

這裡之所以用實現方式這個詞,而不說是優化,是因為即使你加上參數-O0,要求不進行任何優化,GCC也會這麼干。也就是說這就是它的實現方式,不是優化。

講完了GCC的實現方式,來說一下我的看法。對於函數返回值的實現方式,我很喜歡,省掉一次拷貝構造,對時間和內存都有節省,非常不錯。但是,對於它把用返回值拷貝構造新變數的過程給省了,非常不喜歡。對於大多數程序員來講,都會覺得這裡應該有個拷貝構造,而且代碼的意思也很清楚,就是告訴編譯器"你要用這個函數的返回值拷貝構造出新變數"。但是GCC並沒有忠實的按照字面意思編譯代碼,而是假定你的拷貝構造函數就是構造出一個和原變數一樣的變數,於是將拷貝構造省掉了。但是實際上,對於一些特定的類,我們拷貝構造函數不一定就是進行或者只進行"拷貝"這件事,很有可能幹點別的特殊事情。所以,我不喜歡GCC這種自作主張的實現方式。但實際上,大家都知道GCC是這麼乾的,業界大佬們都已經默許了這種實現方式。比如Stephen Prata在它的書《C++ primer plus》中就說這是合理的。但我不敢苟同。


正好遇到這個問題,樓上的解答,很清晰,點個b( ̄▽ ̄)d


推薦閱讀:

C++ 程序員有必要熟練使用除標準庫以外的第三方庫嗎?
為什麼c++的整數會溢出,而Python的整數不會溢出呢?
為什麼scanf()用cmd編譯可以通過,但用vs2015卻不能通過?
有那些值得學習的C/C++和Lua開發的項目源碼?
什麼是面向對象編程?它與面向過程編程的異同有哪些?

TAG:C | 編譯器 |