2011-09-23 70 views
0

考慮下面的代碼:右值引用,指針和拷貝構造函數

int three() { 
    return 3; 
} 

template <typename T> 
class Foo { 
private: 
    T* ptr; 

public: 
    void bar(T& t) { ptr = new T(t); } 
    void bar(const T& t) { ptr = new T(t); } 
    void bar(T&& t) { (*ptr) = t; } // <--- Unsafe! 
}; 

int main() { 
    Foo<int> foo; 

    int a = 3; 
    const int b = 3; 

    foo.bar(a); // <--- Calls Foo::bar(T& t) 
    foo.bar(b); // <--- Calls Foo::bar(const T& t) 
    foo.bar(three()); // <--- Calls Foo::bar(T&& t); Runs fine, but only if either of the other two are called first! 

    return 0; 
} 

我的問題是,爲什麼第三超載Foo::bar(T&& t)崩潰的程序?到底發生了什麼?函數返回後參數t是否被破壞?

此外,我們假設模板參數T是一個非常大的對象,其拷貝構造函數非常昂貴。有沒有辦法使用RValue引用來將它分配給Foo::ptr而不直接訪問這個指針並複製?

+0

你寫的代碼不會導致任何問題(除了內存泄漏,並不實際編譯)。那是你正在使用的實際代碼嗎? –

+0

它編譯好**和**運行良好:http://ideone.com/Ypqxz – Nawaz

+0

這確切的代碼似乎在Visual Studio 2010中正常工作(儘管存在內存泄漏)。你確定你的編譯器版本是否符合右值引用? – Chad

回答

3

在這一行
void bar(T&& t) { (*ptr) = t; } // <--- Unsafe!
你可以解引用未初始化的指針。這是未定義的行爲。 因爲需要爲對象創建內存,所以必須首先調用其他兩個版本的其中一個。
所以我會做ptr = new T(std::move(t));
如果您的類型T支持移動構造函數將被調用。

更新

我建議這樣的事情。不知道你是否需要foo中的指針類型:

template <typename T> 
class Foo { 
private: 
    T obj; 

public: 
    void bar(T& t) { obj = t; } // assignment 
    void bar(const T& t) { obj = t; } // assignment 
    void bar(T&& t) { obj = std::move(t); } // move assign 
}; 

這將避免內存泄漏這也與你的方法很簡單。
如果你真的需要指針在類Foo怎麼樣:

template <typename T> 
class Foo { 
private: 
    T* ptr; 

public: 
    Foo():ptr(nullptr){} 
    ~Foo(){delete ptr;} 
    void bar(T& t) { 
     if(ptr) 
      (*ptr) = t; 
     else 
      ptr = new T(t); 
    } 
    void bar(const T& t) { 
     if(ptr) 
      (*ptr) = t; 
     else 
      ptr = new T(t); 
    } 
    void bar(T&& t) { 
     if(ptr) 
      (*ptr) = std::move(t); 
     else 
      ptr = new T(std::move(t)); 
    } 
}; 
+0

正確,其他版本的欄首先被調用。 –

+0

所以我應該簡單地在'Foo'中創建一個構造函數,它將用'new T()'初始化指​​針,然後使用'Foo :: bar()'的移動構造函數?這會安全嗎? – Zeenobit

0

假設你只稱爲foo.bar(three());其他兩個電話:

爲什麼你認爲最好的工作?您的代碼基本上是相同的:

int * p; 
*p = 3; 

這是不確定的行爲,因爲p沒有指向int類型的有效的變量。

+0

p實際上指向foobar(b)創建的內容,所以它不是未初始化的。 –

+0

@EmilioGaravaglia:這就是爲什麼我在頂部添加了非常粗線條的原因。 –

+0

@ Kerreck SB:噢!我沒有看到! (我的眼睛認爲它是以前的標題的一部分!切勿用粗線開始文本:看起來像屬於別的東西) –

0

沒有理由在代碼中失敗。 ptr將指向先前調用bar所創建的現有int對象,第三個重載將只爲該對象分配新值。

但是,如果你這樣做,而不是:

int main() { 
    Foo<int> foo; 

    int a = 3; 
    const int b = 3; 
    foo.bar(three()); // <--- UB 

    return 0; 
} 

foo.bar(three());線將有不確定的操作(這並不意味着有任何例外),因爲ptr不會是一個有效的指針到int對象。

+0

由於我在飛行中編寫此代碼,我意識到您的意思。你說得對。它運行良好;但只要在任何其他重載之後調用foo.bar(three())'。 – Zeenobit

0

「不安全」的事情,這裏要說的是,分配給PTR新對象之前,你應該擔心的是什麼PTR命運實際上指向。

foo.bar(three()); 

是不安全的,因爲您必須在調用它之前授予 - ptr實際指向某些內容。你的情況,它指向的是什麼被foo.bar(b);

foobar(b)使得ptr創建指向一個新的對象遺忘的foobar(a)

A創建了一個更適當的代碼可以

template<class T> 
class Foo 
{ 
    T* p; 
public: 
    Foo() :p() {} 
    ~Foo() { delete p; } 

    void bar(T& t) { delete p; ptr = new T(t); } 
    void bar(const T& t) { delete p; ptr = new T(t); } 
    void bar(T&& t) 
    { 
     if(!ptr) ptr = new T(std::move(t)); 
     else (*ptr) = std::move(t); 
    } 
} 

;