2011-09-23 67 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); 
    } 
} 

;