【问题标题】:Is the following move constructor code safe?以下移动构造函数代码安全吗?
【发布时间】:2014-02-16 04:55:34
【问题描述】:

这是X类的移动构造函数:

X::X(X&& rhs)
    : base1(std::move(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{ }

这些是我很警惕的事情:

  1. 我们正在从rhs 移动两次,并且不能保证rhs 处于有效状态。 base2 的初始化不是未定义的行为吗?
  2. 我们正在将rhs 的相应成员移动到mbr1mbr2,但是由于rhs 已经被移出(并且不能保证它处于有效状态),为什么要这样做呢?

这不是我的代码。我在一个网站上找到它。这个移动构造函数安全吗?如果有,怎么做?

【问题讨论】:

    标签: c++ c++11 move-semantics


    【解决方案1】:

    这大致是隐式移动构造函数通常的工作方式:每个基础子对象和成员子对象都是从rhs 的相应子对象移动构造的。

    假设base1base2X 的基类,没有构造函数采用X / X& / X&& / const X&,它是安全的。 std::move(rhs) 将在传递给基类初始化程序时隐式转换为 base1&&(分别为 base2&&)。

    编辑:当我在一个与X&& 完全匹配的基类中有一个模板构造函数时,这个假设实际上已经让我痛苦了几次。明确地执行转换会更安全(尽管非常迂腐):

    X::X(X&& rhs)
        : base1(std::move(static_cast<base1&>(rhs)))
        , base2(std::move(static_cast<base2&>(rhs)))
        , mbr1(std::move(rhs.mbr1))
        , mbr2(std::move(rhs.mbr2))
    {}
    

    甚至只是:

    X::X(X&& rhs)
        : base1(static_cast<base1&&>(rhs))
        , base2(static_cast<base2&&>(rhs))
        , mbr1(std::move(rhs.mbr1))
        , mbr2(std::move(rhs.mbr2))
    {}
    

    如果除了base1/base2/mbr1/mbr2 之外没有其他基类或成员,我认为应该完全复制编译器将为X(X&amp;&amp;) = default; 隐式生成的内容。

    再次编辑:C++11 §12.8/15 描述了隐式成员复制/移动构造函数的确切结构。

    【讨论】:

    • 公共服务公告:Casey 的回答和我的一样正确,他领先我几分钟。 :-)
    【解决方案2】:

    这取决于继承层次结构。但是很有可能这段代码很好。这是一个完整的演示,表明它是安全的(对于这个特定的演示):

    #include <iostream>
    
    struct base1
    {
        base1() = default;
        base1(base1&&) {std::cout << "base1(base1&&)\n";}
    };
    
    struct base2
    {
        base2() = default;
        base2(base2&&) {std::cout << "base2(base2&&)\n";}
    };
    
    struct br1
    {
        br1() = default;
        br1(br1&&) {std::cout << "br1(br1&&)\n";}
    };
    
    struct br2
    {
        br2() = default;
        br2(br2&&) {std::cout << "br2(br2&&)\n";}
    };
    
    struct X
        : public base1
        , public base2
    {
        br1 mbr1;
        br2 mbr2;
    
    public:
        X() = default;
        X(X&& rhs)
        : base1(std::move(rhs))
        , base2(std::move(rhs))
        , mbr1(std::move(rhs.mbr1))
        , mbr2(std::move(rhs.mbr2))
        { }
    };
    
    int
    main()
    {
        X x1;
        X x2 = std::move(x1);
    }
    

    应该输出:

    base1(base1&&)
    base2(base2&&)
    br1(br1&&)
    br2(br2&&)
    

    在这里您可以看到每个基地和每个成员都只移动了一次。


    记住:std::move 并没有真正移动。它只是一个右值转换,仅此而已。


    因此代码转换为右值X,然后将其传递给基类。假设基类看起来像我上面概述的那样,那么有一个隐式转换为右值 base1base2,这将移动构造这两个单独的基类。

    还有,


    记住:移出的对象处于有效但未指定的状态。


    只要base1base2 的移动构造函数没有进入派生类并更改mbr1mbr2,那么这些成员仍处于已知状态并准备移动从。没问题。

    现在我确实提到了可能会出现问题。方法如下:

    #include <iostream>
    
    struct base1
    {
        base1() = default;
        base1(base1&& b)
             {std::cout << "base1(base1&&)\n";}
        template <class T>
        base1(T&& t)
            {std::cout << "move from X\n";}
    };
    
    struct base2
    {
        base2() = default;
        base2(base2&& b)
            {std::cout << "base2(base2&&)\n";}
    };
    
    struct br1
    {
        br1() = default;
        br1(br1&&) {std::cout << "br1(br1&&)\n";}
    };
    
    struct br2
    {
        br2() = default;
        br2(br2&&) {std::cout << "br2(br2&&)\n";}
    };
    
    struct X
        : public base1
        , public base2
    {
        br1 mbr1;
        br2 mbr2;
    
    public:
        X() = default;
        X(X&& rhs)
        : base1(std::move(rhs))
        , base2(std::move(rhs))
        , mbr1(std::move(rhs.mbr1))
        , mbr2(std::move(rhs.mbr2))
        { }
    };
    
    int
    main()
    {
        X x1;
        X x2 = std::move(x1);
    }
    

    在这个例子中,base1 有一个模板化的构造函数,它接受一个右值的东西。如果这个构造函数可以绑定到右值X,并且如果这个构造函数会从右值X移动,那么你就有问题了:

    move from X
    base2(base2&&)
    br1(br1&&)
    br2(br2&&)
    

    解决此问题的方法(相对罕见,但并非非常罕见)是forward&lt;base1&gt;(rhs) 而不是move(rhs)

        X(X&& rhs)
        : base1(std::forward<base1>(rhs))
        , base2(std::move(rhs))
        , mbr1(std::move(rhs.mbr1))
        , mbr2(std::move(rhs.mbr2))
        { }
    

    现在base1 看到的是右值base1 而不是右值X,这将绑定到base1 移动构造函数(假设它存在),所以你再次得到:

    base1(base1&&)
    base2(base2&&)
    br1(br1&&)
    br2(br2&&)
    

    世界再好不过了。

    【讨论】:

    • 演示证明它有效,演示不证明它是安全的。 (大概他的代码已经工作了,他在问它是否安全)
    • @MooingDuck:在这个例子中,static_cast&lt;base1&amp;&amp;&gt;(rhs) 也同样有效。在某些情况下,使用forward&lt;T&gt; 比使用static_cast&lt;T&amp;&amp;&gt; 更安全。示例已记录在 open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html ,用例 C 和 F 中。在这些(诚然罕见的)用例中,使用 forward 将失败并出现编译时错误,而使用 static_cast 将失败并运行时间错误。
    • @0x499602D2:等价。我不推荐它。 std::move 通常与推导的模板化参数一起使用。 std::forward 永远不能与推导的模板参数一起使用。将std::move(x) 阅读为:将 x 转换为右值。将std::forward&lt;T&gt;(u) 解读为:将u 转换为T&amp;&amp;,回想一下如果T 是左值引用,则会发生引用折叠。具体来说,std::forward&lt;base1&gt;(rhs) 表示:将rhs 转发为右值base1static_cast&lt;base1&amp;&amp;&gt;(rhs) 也说了同样的话。
    • 我还是不明白为什么语言特性“cast x to rvalue”被命名为std::move
    • @LightnessRacesinOrbit:我试图保持礼貌。现在我说得更直白一点:包括你自己在内的每个人都有足够的时间来反对,但没有及时反对。也许2002年早于你的时代,我不知道。但是该设计经历了非常长的设计审查时间。 move 的命名从未被认为是有争议的,直到为时已晚。这并不是说基础物理学是错误的。这只是一个设计细节,十年来没有人认为它足够重要,或令人反感到足以抱怨。
    【解决方案3】:

    不,这将是可能未定义行为的示例。它可能在很多时候都有效,但不能保证有效。 C++ 允许这样做,但您必须确保不要以这种方式再次尝试使用rhs。在 它的内脏被撕掉 它的 rvalue 引用被传递给一个可能从它移动的函数之后,您尝试使用 rhs 三次(使其处于未定义状态)。

    【讨论】:

    • @MooingDuck 感谢您指出我的错误。我知道在某个时候,但我忘记了。我的答案现在更正确了吗?
    • 哦,等等,我没想到base1base2 可以从X&amp;&amp; 构造。在这种情况下,您的答案是正确的,它可能未定义,具体取决于 base1base2 的构造函数。我只是没有建立base1 构造函数可能X&amp;&amp; 隐式转换为base1&amp;&amp; 的连接。抱歉造成不必要的混乱。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-09
    • 1970-01-01
    • 2012-10-31
    • 2012-10-17
    • 2011-05-22
    • 2010-09-05
    相关资源
    最近更新 更多