【问题标题】:Move constructor for a map移动地图的构造函数
【发布时间】:2010-10-27 14:49:41
【问题描述】:

我想我确实了解移动语义的“基本思想”,但现在当我处于实现自己的地图的阶段时,我停下来开始思考,当我要编写一个用例并走路时通过移动地图的ctor。如果我错了,请纠正我,但我确实理解移动语义的整个业务是如何工作的,他们认为它们有助于避免不必要的复制?正确的?现在,以地图为例,仅出于本示例的目的,假设我的地图建模为:

class Map
{
Link* impl_;//THIS IS A POINTER TO A LINK WHICH HAS A parent, left and right (of Link type)
Map(Map&& tmp);//move ctor
//unnecessary code ommited
};

这里是障碍: 当我试图为我的地图考虑移动 ctor 时,我看不到一种方法可以避免为所有需要创建的链接分配新空间,然后将它们的指针与来自 tmp Map 对象的指针交换(作为 arg给我的移动 ctor)。
所以无论如何我都必须分配空间,不是吗?

【问题讨论】:

    标签: c++ move-semantics


    【解决方案1】:

    您所要做的就是重新分配链接指针,因为所有其他链接指针都附加到它,它们现在将成为新地图的一部分。

    Map(Map&& tmp) :impl_(tmp.impl_) { tmp.impl_ = nullptr; }
    

    这是假设没有其他数据成员。

    【讨论】:

    • 是的,我刚刚在完成写作过程中遇到了问题。谢谢。
    【解决方案2】:

    您总共需要五个操作:经典的“三巨头”(复制构造函数、复制赋值运算符、析构函数)和两个新的移动操作(移动构造函数、移动赋值运算符):

    // destructor
    ~Map();
    
    // copy constructor
    Map(const Map& that);
    
    // move constructor
    Map(Map&& that)
    {
        impl_ = that.impl_;
        that.impl_ = 0;
    }
    
    // copy assignment operator
    Map& operator=(const Map& that);
    
    // move assignment operator
    Map& operator=(Map&& that)
    {
        using std::swap;
        swap(impl_, that.impl_);
        return *this;
    }
    

    移动赋值运算符背后的基本思想是,如果您在执行交换后不再次检查map2swap(map1, map2)map1 = map2 具有相同的可观察到的副作用。 Recall that an rvalue is either a prvalue or an xvalue。根据定义,客户端不能检查由纯右值指定的对象两次,因为评估纯右值总是会导致创建新对象。观察此技巧的唯一方法是从诸如 std::move(map_variable) 之类的 xvalue 中移动,但很明显map_variable 可能会被修改。

    如果您希望即使在复制时也能进行异常安全赋值,您可以复制赋值运算符(采用const Map&)和移动赋值运算符(采用Map&&)组合成一个通用赋值运算符(以Map)。那么你总共只需要四个操作:

    // exception safe copy/move assignment operator
    Map& operator=(Map that)
    {
        using std::swap;
        swap(impl_, that.impl_);
        return *this;
    }
    

    请注意,赋值运算符的这种变体采用按值的参数。如果参数是左值,复制构造函数初始化that,如果参数是右值,移动构造函数完成这项工作。 (另请注意,如果您已经提供了移动操作,则专门化 std::swap 不太可能带来进一步的显着性能提升。)

    【讨论】:

    • 谢谢你的回答,一个问题我没看清楚,为什么我不需要复制赋值运算符和移动赋值运算符而只有这个?您描述的最后一个。
    • @There:你知道拷贝构造函数什么时候调用,什么时候移动构造函数调用吗?请参阅this answer 以获取对此“技巧”的更深入解释(如果您了解参数传递只是另一种形式的初始化,这并不是真正的技巧)。
    • 知道了。在传递左值的情况下,常规复制 ctor 将完成这项工作,而在传递右值的情况下,将移动 ctor 传递给它。当然,如果我错了,请纠正我。
    • @There:正如我上一段所述,就是这样:“如果参数是左值,复制构造函数会初始化that,如果参数是右值,则移动构造函数会这份工作。”
    【解决方案3】:

    除了不重新发明现有容器的标准免责声明之外,不进行任何分配而简单地分配根节点指针是否就足够了?

    【讨论】:

      【解决方案4】:

      可以用旧的 C++(不是 0x)实现移动语义,但必须显式地完成并且比较棘手。

      class X
      {
      // set access specifiers as required
         struct data
         {
            // all the members go here, just plain easy-to-copy members
         } m;
      
         data move()
         {
            data copy(m);
            m.reset(); // sets everything back to null state
            return m;
         }
      
         explicit X( const data& d ) : m(d)
         {
         }
        // other members including constructors
      };
      
      X::data func() // creates and returns an X
      {
        X x; // construct whatever with what you want in it
        return x.move();
      }
      
      int main()
      {
         X x(func());
         // do stuff with x
      }
      

      并且 X 可以在上面设置为不可复制和不可分配,数据可以在堆上创建项目,并且 X 的析构函数负责清理。当 move() 函数重置数据时,partition X 将无需清理任何内容,因为所有权已转移。

      通常数据结构在 X 中应该是公共的,但它的所有成员应该是私有的,X 是朋友。因此,用户不会直接访问其中的任何内容。

      请注意,如果您在 X 上调用 move() 而不将其附加到另一个 X,则可能会“泄漏”,因此如果您仅在上面调用 func() 就会泄漏。如果 X 的构造函数 from data throws 你也应该小心(它的析构函数不会被调用,所以不会自动进行清理)。如果数据的复制构造函数本身给你带来了更大的麻烦。 通常这些都不应该发生,因为数据包含轻的东西(指针和数字)而不是重的对象。

      【讨论】:

        猜你喜欢
        • 2011-05-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-03
        • 1970-01-01
        • 1970-01-01
        • 2014-11-08
        相关资源
        最近更新 更多