移动构造函数未按预期被调用

14

我刚接触C++0x,正试图理解右值引用和移动构造函数。我正在使用带有-std=c++0x选项的g++ 4.4.6,并对以下代码感到困惑:



    class Foo 
    {
    public:
      Foo() 
        : p( new int(0) )
      {
        printf("default ctor\n");
      }

      Foo( int i )
        : p( new int(i) )
      {
        printf("int ctor\n");
      }

      ~Foo() 
      {
        delete p;
        printf("destructor\n");
      }

      Foo( const Foo& other ) 
        : p( new int( other.value() ) )
      {
        printf("copy ctor\n");
      }


      Foo( Foo&& other )
        : p( other.p )
      {
        printf("move ctor\n");
        other.p = NULL;
      }

      int value() const 
      {
        return *p;
      }

    private:
      // make sure these don't get called by mistake
      Foo& operator=( const Foo& );
      Foo& operator=( Foo&& );

      int* p;
    };


    Foo make_foo(int i) 
    {
      // create two local objects and conditionally return one or the other
      // to prevent RVO
      Foo tmp1(i);
      Foo tmp2(i);

      // With std::move, it does indeed use the move constructor
      //  return i ? std::move(tmp1) : std::move(tmp2);
      return i ? tmp1 : tmp2;

    }


    int main(void) 
    {
      Foo f = make_foo( 3 );

      printf("f.i is %d\n", f.value());

      return 0;
    }

我发现编译器在main()函数中使用复制构造函数来构建对象。当我在make_foo()函数中使用std::move时,主函数中使用了移动构造函数。为什么在make_foo()函数中需要使用std::move呢?我认为虽然tmp1和tmp2是make_foo()函数中的命名对象,但当它们从函数返回时应该成为临时对象。


在 "return i ? tmp1 : tmp2;" 中,tmp1和tmp2不是右值。因此,编译器无法自动决定将值从对象中“移出”,因为它们可能稍后/在其他地方使用,但其内容已被破坏(即使它们在您的示例中没有)。因此,您必须使用std::move函数手动使它们成为右值,以表示我很高兴让您将值从这些对象中移出。至少这是我对它如何工作的理解。 - jcoder
1
去掉复制构造函数后会出现以下错误(gcc 4.6.3):cannot bind Foo lvalue to Foo&&,这表明编译器仍将返回值视为左值。 - stefaanv
在C++11中,您可以使用关键字delete来显式删除默认赋值运算符,而不仅仅是将它们定义为未定义(Foo& operator=( const Foo& ) = delete;)。这样,如果您尝试使用它们,您将得到编译器错误,而不仅仅是链接器错误。 - Gorpik
将它们设为私有也会导致编译器错误。不过我同意 "= delete" 更清晰明了。就像我说的,我对 C++11 还很陌生 :-) - user1806566
1个回答

11

这是您的问题:

return i ? tmp1 : tmp2;

如果在函数中的返回语句仅为return var;时,该局部变量才会被移动。如果你想进行这种测试,你需要使用if:

if (i) {
   return tmp1;
} else {
   return tmp2;
}
引文有些复杂,但它在12.8/31和12.8/32中。即使表达式是左值,当符合12.8/31的标准时,它也被视为右值;在该条件下,选择拷贝构造函数的重载解析首先会按照对象是右值的情况执行。12.8/31是指:在具有类返回类型的函数中的return语句中,如果表达式是一个非易失性自动对象(不是函数或catch子句参数)的名称,并且该对象与函数返回类型具有相同的cv-unqualified类型,则可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作。这意味着return tmp;允许进行拷贝省略,但return (cond?tmp:tmp);则不允许。请注意,为了使编译器在return语句中生成隐式的std::move,返回的对象必须是拷贝省略的候选对象,除非它也是函数的参数。使用条件操作抑制了拷贝省略,并同时阻止编译器从您的对象中进行移动。第二种情况可能更容易编写:
Foo make_foo(Foo src) {
   return src;           // Copy cannot be elided as 'src' is an argument
}

你有引证吗?我正在研究这个,并且我只能找到使用上述样式示例的人们,作为避免 RVO 并导致隐式移动发生的一种方式。我没有在标准中找到清楚表明编译器何时可以隐式移动的措辞。(另一方面,我相信隐式移动始终是可选的,因此编译器可以说只有在直接执行 return foo; 时才会这样做,或者仅在非平凡优化级别时才会这样做等)。 - Yakk - Adam Nevraumont
谢谢。如果我用if替换?:,它确实会做我期望的事情。如果您有引用,我会很感兴趣。我还没有在其他地方看到关于何时可以隐式移动局部变量以进行返回的规则的规定。 - user1806566
@Kevin:哦,那很简单。你会得到一个悬空的rvalue引用到Foo,因为它是对本地对象的引用,任何尝试使用它的行为都将是未定义的 :). 通常,返回引用的函数要么是输入参数的引用,要么是某个非本地变量的引用。 - Dave S
@Kevin:如果返回类型是Foo&&,那么它将是未定义的行为,就像在返回对本地对象的引用(*lvalue-rvalue-*都无关紧要)的任何其他情况一样。 - David Rodríguez - dribeas
@David Rodríguez:帮我理解一下,如果我们调用“return temp”,根据引文,复制/移动是否应该被省略。阅读了这篇引文后,我的期望是不会调用复制/移动构造函数,而是直接在返回值中构建。但我猜你的意思是它会导致调用移动构造函数? - cexplorer
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接