天天看點

一個關于臨時對象的BUG

轉自: https://blog.csdn.net/TeddyWing/article/details/13170

(部落客看完這篇部落格之後,感覺自己不會C++了,嗚嗚嗚)

我相信任何一個使用C++超過一定時間的程式員都不會否認這樣一個事實:使用C++需要有足夠的技巧。它充滿了有各種各樣的難以識别的陷阱,頃刻就可以讓一段看起來毫無破綻的代碼崩潰。例如,對C/C++的新手而言,學會如何考慮對象的生存期就是他們必須跨越的一個障礙,這方面最典型的問題,就是對對象指針的使用,特别是在使用一個已經被删除了的對象指針的時候:

MyClass *mc = new MyClass;


// Do some stuff


delete mc;


mc->a = 1;      // Uh oh...mc is no longer valid!           

一些更玄妙的事情發生在函數傳回的時候,我們假設一個函數,例如foo()傳回一個MyClass類型的對象引用:

MyClass &foo() {


MyClass mc;


// Do some things


return mc;


}           

這段有問題的代碼實際上是完全合法的,當函數foo()的生存期還沒有結束的時候,mc就會被銷毀掉,但函數傳回的是它的一個引用,這樣一來,函數的調用者将得到一個引用,它指向一個已經不存在對象,如果你運氣夠好,你也許可以得到一個來自編譯器的警告(例如VC 7.0将給出這樣一個警告:“warning C4172: returning address of local or temporary.”),但注意不是每種編譯器都會這麼友好。

這是一個很常見的例子,我相信每個C++程式員都至少犯過一次這樣的錯誤。然而,對于C++的臨時對象而言,事情變得稍微有點複雜。如果我将foo的定義稍稍變一下,讓它傳回一個對象的拷貝,而不是引用,會發生什麼情況?

MyClass foo()  {


MyClass mc;


return mc;


}           

現在,當foo傳回的時候,它将生成一個臨時對象,這個臨時對象将被賦給調用函數指定的一個變量。

為了看看這一切是如何發生的,我們看看Listing 1代碼的執行結果:

// ConsoleApplication2.cpp : 此檔案包含 "main" 函數。程式執行将在此處開始并結束。
//

#include "pch.h"
#include <iostream>
using namespace std;



// Demonstrates returning a temporary object.

#include <iostream>
using namespace std;
class MyClass {
public:

	MyClass(const MyClass &)  {
		cout << "Copy constructor\n";
	}

	MyClass()  {
		cout << "Default constructor\n";
	}

	MyClass &operator=(const MyClass &)  {
		cout << "Assignment operator\n";
		return *this;
	}

	~MyClass() {
		cout << "Destructor\n";
	}
};


MyClass foo() {
	MyClass mc;

	// Return a copy of mc.
	return mc;
}

int main()  {
	// This code generates the temporary
	// object directly in the location
	// of retval;
	MyClass rv1 = foo();
	cout << "----------------------------\n";
	// This code generates a temporary
	// object, which then is copied
	// into rv2 using the assignment
	// operator.
	MyClass rv2;
	rv2 = foo();
	cout << "Returned from foo\n";
	return 0;
}

           
一個關于臨時對象的BUG

你也許會想,這裡是不是有一個對象丢失了,畢竟,如果當你看了僞代碼以後,你會認為這樣一些事情是應該發生的:

在foo中,mc被聲明了,它調用了預設的構造函數,然後,foo傳回了一個臨時對象,這個臨時對象是對mc的拷貝,并是以調用了拷貝構造函數,這個臨時對象被指派給了rv1,并再次調用拷貝構造函數。

但是請等一下,我們檢視應用程式的輸出,拷貝構造函數卻隻被調用了一次!而本來應該有三個對象生成:mc(在foo函數中),一個臨時對象,以及rv1。為什麼不是調用三次構造函數?這個問題的答案就是:C++标準所允許的代碼優化欺騙了我們,這樣做的目的是為了避免代碼過于低效,如果一個臨時對象作為傳回值被立即賦給另一個對象,這個臨時對象本身将被構造到被指派對象在記憶體中的位置。這樣避免了一次無謂的構造函數調用,當構造函數需要做很多初始化工作的話,這樣可以節省不少時間(如果你對這方面的内容很感興趣,請參考C++标準的第12.2節,第3段)。

另外有一個相關的例子,例如,當rv已經被聲明了:

MyClass rv;
rv = foo();           

這時候,臨時對象将不被構造到rv的位置,因為發生foo調用的時候,rv已經被構造過了,是以,這個臨時的傳回值必須被做為一個獨立的對象來構造,然後再指派給rv。實際上,如果你将Listing 1代碼中的注釋打開,你将會得到這樣一些期待的結果:

Default constructor (rv2)

Default constructor (mc)

Copy constructor (temporary)

Destructor (mc)

Assignment operator (rv2 = temporary)

Destructor (temporary)

Returned from foo

Destructor (rv2)           

這裡需要注意的是臨時對象在它被生成的表達式執行結束的時候被銷毀,換句話說,析構函數将在這句話執行的末尾被調用:

rv = foo();   // Temporary is destroyed here           

這一切看起來都非常美妙,但是如果是下面這個例子,會發生什麼情況呢?

MyClass &mc = foo();           

(上面那句在我的編譯器visual studio裡面報錯了)

現在将不是将臨時對象拷貝到新的對象上面,我僅僅是将它指派給一個引用,(請注意,這和最開始那個例子有一點差別,在第一個例子裡面,我将一個局部變量的引用做為了函數傳回值,而在這個例子裡,我是将一個函數傳回的臨時變量的引用指派給一個變量)。那麼,現在将會發生什麼情況呢?臨時對象将在什麼時候被銷毀呢?如果它還是在表達式執行的執行結束的時候被銷毀,就如同上面那個例子一樣,這段代碼将因為一個指向不存在的對象的應用而徹底完蛋。但是,請注意,C++标準同意對這種情況提出一種不同尋常的解決辦法:如果一個臨時對象被指派給一個引用,這個臨時對象在這個引用的生命周期中将不能被銷毀。換句話說,不同于傳回一個局部變量的引用,将一個引用綁定到一個臨時對象上是完全合法的,任何時候使用這個引用,這個對象都應該是有效的。

The BUG

我猜我隻能說僅僅當編譯器恰當的實作了C++标準,這個引用才可能是有效的。Eugene Gershnik将Listing 2所列的代碼發送給了我,它有一個叫foo的函數,傳回了一個std::vector<char>類型的臨時對象,并且這個對象被指派給了一個引用,當這段程式在VC7的Release模式下編譯并運作時并沒有問題,但是當它在Debug模式下運作時,我得到了這樣一個錯誤:

“The instruction at “0x004121b5”referenced memory at “0x00000000”.The memory could not be “read”.           
#include "pch.h"
#include <iostream>
#include <cstdio>
#include<vector>
//Assigning a reference to a temporary object

// Problem with reference bound to temporary
// The function foo returns a temporary object
// of type std::vector<char>, which is then bound
// to a reference of type const std::vector<char>&.
// When the expression "int m[80] = {0};" is
// executed,
// the reference bar no longer seems to be valid,
// and the program will crash in the call to
// printf.
// Removing the line "int m[80] = {0};" eliminates
// the problem.
//
// Compile with VC7, with the "Program Database
// for Edit and Continue" debug option.

std::vector<char> foo() {
	std::vector<char> ret(20);
	return ret;
}

int main() {
	const std::vector<char> &bar = foo();
	int m[80] = { 0 };
	std::printf("%d\n", bar[0]);
	return 0;
}
           

(部落客的visual studio并沒有報錯,還給了傳回的結果)

一個關于臨時對象的BUG

開始的時候我猜想由于Release版本對“int m[80]= {0};”這一行的優化造成了這種結果,因為這行代碼确實什麼也沒有做,我認為這種優化消除了在Release下的錯誤。但是當我将代碼發給Microsoft(我去,都是大佬啊),并征求他們的意見的時候,Jeff Peil給我回了信,并指出了真正的原因所在:

這個問題是由于編輯-繼續調試支援(edit-and-continue debugging support)的功能而造成的。這個錯誤将在即将釋放的下一版的Visual C++中得到修正,你可以通過将編輯-繼續調試支援從編譯選項中去掉而避免這個錯誤發生(編譯時仍然會産生調試資訊,你需要做的僅僅是将/ZI選項替換成 /Zi選項)。當然,另外一個解決辦法就是不将引用綁定到臨時對象上去, 這樣你可以繼續使用編輯-繼續調試支援功能,就像下面的代碼那樣:

int main()  {

std::vector<char> bar;

std::swap(bar,foo());

int m[80]={0};

std::printf(“%d/n”, bar[0]);

return 0;

} 

                                                                            —Jeff Peil           

雖然性能上有一點點損失,但Jeff的代碼工作的很好。交換這兩個Vector的内容粗看起來是一個代價昂貴的操作,但實際上所有的交換工作就是交換兩個數組中很少的内部變量,這其中并不涉及到緩沖區的拷貝,是以交換将在常量時間内完成。

當然如果你想簡單的解決這個問題,可以将表達式“int m[80]= {0};”移動到聲明變量bar之前。因為它們之間不存在什麼倚賴關系,先聲明任何一個都是沒有關系的。

繼續閱讀