【问题标题】:What is the behaviour of compiler generated move constructor?编译器生成的移动构造函数的行为是什么?
【发布时间】:2013-04-09 22:27:03
【问题描述】:

std::is_move_constructible<T>::value == true 是否暗示T 有一个可用的移动构造函数? 如果是这样,它的默认行为是什么?

考虑以下情况:

struct foo {
    int* ptr;
};

int main() {
    {       
        std::cout << std::is_move_constructible<foo>::value << '\n';
        foo f;
        f.ptr = (int*)12;
        foo f2(std::move(f));
        std::cout << f.ptr << ' ' << f2.ptr << '\n';
    }
    return 0;
}

输出是:

1
0000000C 0000000C

我认为f.ptr 应该是nullptr。 所以在这种情况下,

  1. f2 move 构造了吗?
  2. 如果是,右值不应该失效吗?
  3. 我如何知道一个类的实例是否可以正确移动构造(使旧实例无效)?

(我正在使用 VS11。)

更新

移动构造函数的默认行为与复制构造函数相同,是否正确? 如果是真的,

  1. 我们总是希望移动 ctor 窃取已移动对象的资源,而默认移动 ctor 的行为与预期不符,那么拥有默认移动 ctor 有什么意义?
  2. 我如何知道一个类是否有自定义移动构造函数(可以保证其行为正常)?

当我声明一个时,foo f2(std::move(f)); 似乎调用了复制 ctor,请参阅:

struct foo {
    int* ptr;
    foo() {}
    foo(const foo& other) {
        std::cout << "copy constructed\n";
    }
};

int main() {
    {       
        std::cout << std::is_move_constructible<foo>::value << '\n';
        foo f;
        foo f2(std::move(f));
    }
    system("pause");
    return 0;
}

现在的输出是:

1
copy constructed

如果foo 有一个移动构造函数,那么foo f2(std::move(f)) 不会调用它吗?

所以现在我的问题是: 如何知道一个类是否有移动 ctor,如果有,我如何显式调用它?

我想做的是……

template<typename T, bool has_move_ctor>
struct MoveAux;

template<typename T>
struct MoveAux<T, true> {
    static void doMove(T* dest, T* src) {
        new(dest) T(std::move(*src)); //move ctor
    }
};

template<typename T>
struct MoveAux<T, false> {
    static void doMove(T* dest, T* src) {
        new(dest) T(*src); //copy ctor
        src->~T();
    }
};

template<typename T>
inline doMove(T* dest, T* src) {
    MoveAux<T,/*a trait*/>::doMove(dest, src);
}

所以我认为std::is_move_constructible&lt;T&gt;::value 可以传递给模板,而现在我看到这个特征只关心T t(T()) 是否是一个有效的表达式,它可能会调用T::T(const T&amp;)。 现在假设T 是一个自定义类,那么我希望上述模板的行为类似于:

  1. 如果我不声明移动 ctor,我希望该模板方法调用 MoveAux&lt;T,false&gt;::doMove
  2. 如果我声明了一个,我需要它调用MoveAux&lt;T,true&gt;::doMove

有没有可能实现这个功能?

【问题讨论】:

  • @DavidSchwartz:“将对象传递给move 后,您可能无法访问其任何成员”是不正确的。从一个对象移动后,您可能无法访问它的任何成员,这甚至不是真的。从原始类型移动的效果是使其保持不变。类类型可以自己决定移动的效果——标准库类大多说对象处于不确定但有效的状态,通常至少析构函数需要工作,这意味着任何析构函数访问的成员将必须持有合理的值。
  • @DavidSchwartz:“在传递一个要移动的对象后,您可能无法访问它的任何成员。”这根本不是真的。上述代码具有 C++ 规范明确定义的行为。移动构造函数将复制指针,从而定义原始对象的状态。

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


【解决方案1】:

std::is_move_constructible&lt;T&gt;::value == true 是否暗示T 有一个可用的移动构造函数?

移动构造函数或复制构造函数。请记住,复制构造的操作满足操作移动构造的所有要求,甚至更多。

在标准术语中,MoveConstructible 对象是对其表达式求值的对象:

T u = rv; 

使u等于构造前rv的值; rv 被移出后的状态是未指定。但由于未指定,这意味着状态甚至可能与 rv 的状态相同 before 被移出:换句话说,u 可能是一个副本rv

事实上,标准将CopyConstructible 概念定义为MoveConstructible 概念的改进(所以CopyConstructible 也是MoveConstructible,但反之则不然) .

如果是,它的默认行为是什么?

隐式生成的移动构造函数的行为是对生成它的类型的数据成员执行逐个成员移动。

根据 C++11 标准的第 12.8/15 段:

非联合类X的隐式定义复制/移动构造函数执行成员方式复制/移动 其基地和成员。 [ 注意:brace-or-equal-initializers 的非静态数据成员被忽略。看 也是 12.6.2 中的示例。 ——尾注]

此外:

1 - f2 move 构造了吗?

是的。

2 - 如果是,右值不应该失效吗?

移动一个指针与复制它是一样的。所以没有失效发生,也不应该发生。如果你想要一个移动构造函数将被移动对象留在特定状态(即将指针数据成员设置为nullptr),你必须编写自己的 - 或将此责任委托给一些智能指针类,例如@987654339 @。

注意,“invalidated”这个词在这里并不完全正确。移动构造函数(以及移动赋值运算符)旨在使移动对象保持有效(但未指定)状态。

换句话说,需要尊重类不变量 - 并且应该可以调用对其状态没有任何先决条件的移动对象操作(通常是销毁和赋值)。

【讨论】:

  • std::is_move_constructible&lt;T&gt;::value == true 并不意味着存在移动构造函数。
  • 比编写自己的移动构造函数更好:使用 std::unique_ptr。
  • @SebastianRedl:当然。但是 OP 正试图理解移动构造的基础知识,所以告诉他“使用为你处理移动语义的东西”,尽管它在设计上是正确的,但不会是对他们的问题的深刻回答。
  • @AndyProwl:我在这里反对“你必须自己写”。虽然在答案的上下文中,它可能被解释为“编译器生成的移动构造函数不会为你做原始指针”,它也可能被解释为“没有其他方法”。
  • 如果移动构造函数是private,那么它可以满足is_copy_constructible,但不能满足is_move_constructible。这似乎与这个关于 CopyConstructible 暗示 MoveConstructible 的答案相矛盾。 警告:我正在使用来自en.cppreference.com/w/cpp/types/is_copy_constructibleis_{copy,move}_constructible 的“可能实现”,也许它们不正确? (clang 3.3)
【解决方案2】:

std::is_move_constructible::value == true 是否意味着 T 有一个可用的移动构造函数?

没有。它指出您可以take an rvalue expression of the object type and construct an object from it。这是否使用移动构造函数或复制构造函数与此 trait 无关。

f2 move 构造了吗?

是的。

如果是这样,右值不应该失效吗?

没有。运动不是这样运作的。

我如何知道一个类的实例是否可以正确移动构造(使旧实例无效)?

这不是存在的“正确移动构造”的任何定义。如果你想“使旧的无效”,那么你必须自己做。

移动构造通常不保证关于旧对象的状态。它将处于有效但未定义的状态。这种状态很可能“和以前一样”。指针的移动构造与复制指针相同。

如果您想在移动后“无效”,那么您需要编写自己的移动构造函数来显式执行此操作。

(我用的是VS11)

那么你根本就没有编译器生成的移动构造函数。没关系,因为指针的移动和复制构造函数都做同样的事情。

【讨论】:

  • 如何知道 T 是否有自定义的移动构造函数? (!std::is_trivially_move_constructible::value && std::is_move_constructible::value) 会成功吗?
  • @Frahm:这也行不通,因为具有非平凡复制构造函数的对象也将是非平凡可移动的。没有特征可以检测用户提供的移动构造函数的显式提供。还有,你为什么要关心?用户提供与否,只需调用它
  • 我想编写一个专门用于指示 T 是否可以正确移动的特征的类,如果不能,它将回退到 detor + copy ctor,而如果 T(hold resources) 有一个自定义复制 ctor 和编译器生成的移动 ctor,然后调用该方法将不会按预期运行。
  • @Frahm:这是您添加的条款的虚构“正确移动”,还是实际“正确移动”?如果用户给你一个你打算移动的类型,并且由于他们未能正确实现移动而导致移动实际上不起作用,那不是你的错。因此,只需通过始终移动对象以正确的方式实现您的代码。此外,即使您可以检测到用户提供的移动构造函数,也不能保证它被正确实现
【解决方案3】:

移动构造函数的默认行为与复制相同 构造函数,正确吗?如果是真的

没有。这是不对的。这仅适用于基元。类似于拷贝构造函数。

默认生成的复制构造函数按照声明的顺序调用其所有成员的复制构造函数

但是默认生成的move构造函数会按照声明的顺序调用其所有成员的move构造函数

现在下一个问题是,原语ints floats pointers 的复制/移动构造函数是做什么的?

答案:他们只是复制值(复制和移动构造函数)

【讨论】:

    【解决方案4】:

    请注意,Visual Studio 2012 / VC++11 确实支持编译器生成的移动构造函数;事实上,考虑一下"C++11 Features in Visual C++ 11" 博客文章中的这句话(重点是我的):

    右值引用v3.0新增规则自动生成move 特定条件下的构造函数和移动赋值运算符。 这个不会在VC11中实现,后续会继续 VC10 从不自动生成招式的行为 构造函数/移动赋值运算符

    使用原始指针,您必须自己定义移动构造函数,手动清除旧的“moved-from”指针:

    class Foo 
    {
    public:
    
        // Move constructor
        Foo(Foo&& other)
            : m_ptr(other.m_ptr) // copy pointer value
        {
            // Clear out old "moved-from" pointer, to avoid dangling references
            other.m_ptr = nullptr;
        }
    
    private:
        int* m_ptr;
    };
    

    相反,如果您使用像std::unique_ptr 这样的智能指针,则正确定义了移动构造函数,您只需调用std::move

    class Foo 
    {
    public:
    
        // Move constructor
        Foo(Foo&& other)
            : m_ptr(std::move(other.m_ptr)) // move from other, 
                                            // old pointer automatically cleared
        {
        }
    
    private:
        std::unique_ptr<int> m_ptr;
    };
    

    使用自动生成移动构造函数,您不必显式定义自定义移动构造函数,如果member-wise移动适合您。

    【讨论】:

      【解决方案5】:

      n3376 12.8/15

      非联合类 X 的隐式定义复制/移动构造函数执行memberwise复制/移动 它的基地和成员。

      每个基础或非静态数据 成员以适合其类型的方式复制/移动:

      ——如果成员是一个数组,每个元素直接用x对应的子对象初始化;

      ——如果成员 m 具有右值引用类型 T&&,则使用 static_cast(x.m) 直接初始化它;

      ——否则,基数或成员直接用 x 的相应基数或成员初始化。

      【讨论】:

        【解决方案6】:

        如果 foo 有一个移动构造函数,那么 foo f2(std::move(f)) 不会调用它吗? 当您提供复制构造函数时,您不会获得默认的移动构造函数。添加以下行以获取它(并注意更改)。 foo(foo&& ifm)=default;

        【讨论】:

          猜你喜欢
          • 2019-04-16
          • 2017-07-20
          • 1970-01-01
          • 2012-10-21
          • 1970-01-01
          • 1970-01-01
          • 2021-10-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多