为什么没有默认的移动分配/移动构造函数?


89

我是一个简单的程序员。我的班级成员变量通常由POD类型和STL容器组成。因此,我很少需要编写赋值运算符或复制构造函数,因为它们是默认实现的。

此外,如果我std::move在不可移动的对象上使用它,则使用赋值运算符,这std::move是绝对安全的。

因为我是一个简单的程序员,所以我想利用移动功能,而不必在我编写的每个类中添加移动构造函数/赋值运算符,因为编译器可以将它们简单地实现为“ this->member1_ = std::move(other.member1_);...

但这不是(至少在Visual 2010中不是),是否有任何特殊原因?

更重要的是; 有什么办法可以解决这个问题?

更新: 如果您不看GManNickG的答案,他为此提供了一个很好的宏。如果您不知道,如果您实现了移动语义,则可以删除掉swap成员函数。


5
您知道您可以让编译器生成默认的移动ctor
aaronman 2013年

3
std :: move不执行移动,它只是从l值转换为r值。该移动仍由move构造函数执行。
Owen Delahoy,2015年

1
你在说什么MyClass::MyClass(Myclass &&) = default;
桑德堡

是的,如今:)
Viktor Sehr

Answers:


76

移动构造函数和赋值运算符的隐式生成一直存在争议,并且在C ++标准的最新草案中进行了重大修订,因此,当前可用的编译器在隐式生成方面的行为可能会有所不同。

有关问题历史的更多信息,请参见 2010 WG21论文列表并搜索“ mov”

当前规范(N3225,从11月开始)规定为(N3225 12.8 / 8):

如果类的定义X未明确声明移动构造函数,则仅当且仅当将隐式声明为默认构造函数。

  • X 没有用户声明的副本构造函数,并且

  • X 没有用户声明的副本分配运算符,

  • X 没有用户声明的移动分配运算符,

  • X 没有用户声明的析构函数,并且

  • move构造函数不会隐式定义为Delete。

12.8 / 22中有类似的语言指定何时将移动分配运算符隐式声明为默认值。您可以在N3203中找到支持当前隐式移动生成规范所做的更改的完整列表:收紧生成隐式移动的条件,该条件 主要基于Bjarne Stroustrup的论文N3201提出的解决方案之一:顺着移动


4
我写了一篇小文章,上面有一些图,描述了隐式(移动)构造函数/分配的关系:mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny 2011年

唉所以每当我必须定义多态基类的析构函数空只是为了将其指定为虚拟的缘故,我必须明确地定义移动构造函数和赋值运算符,以及:(。
someguy

@James McNellis:我以前尝试过这种方法,但是编译器似乎不喜欢它。我本打算在此回复中发布错误消息,但是在尝试重现错误后,我意识到它提到了该错误消息cannot be defaulted *in the class body*。因此,我在外面定义了析构函数,并且它起作用了:)。不过,我觉得有些奇怪。有人解释吗?编译器是gcc 4.6.1
someguy 2011年

3
既然已经批准了C ++ 11,也许我们可以对此答案进行更新?好奇什么行为会赢。
Joseph Garvin 2014年

2
@Guy Avraham:我想我已经说了7年了,如果我有一个用户声明的析构函数(甚至是一个空的虚拟析构函数),则不会将Move构造函数隐式声明为默认值。我想那会导致复制语义?(我已经好几年没有接触过C ++了。)James McNellis然后评论说这virtual ~D() = default;应该起作用,并且仍然允许隐式move构造函数。
someguy

13

该标准已考虑了隐式生成的move构造函数,但可能很危险。请参阅Dave Abrahams的分析

最后,尽管有很多限制,但该标准确实包括隐式生成move构造函数和move赋值运算符:

如果类X的定义未明确声明移动构造函数,则仅当且仅当
-X没有用户声明的副本构造函数,
-X没有用户声明的副本赋值运算符,才会隐式声明为move构造函数。,
-X没有用户声明的移动分配运算符,
-X没有用户声明的析构函数,并且
-move构造函数不会隐式定义为Delete。

但这还不是故事的全部。可以声明一个ctor,但仍将其定义为delete:

隐式声明的copy / move构造函数是其类的内联公共成员。如果X具有以下功能,则默认将类X的默认复制/移动构造函数定义为已删除(8.4.3):

—具有非平凡的对应构造函数且X是类联合类的变体成员,
—类类型为M(或其数组)的非静态数据成员,由于重载解析(13.3)而不能被复制/移动,例如应用于M的相应构造函数,会导致模棱两可或从默认构造函数中删除或无法访问的函数
即直接或虚拟基类B,由于重载分辨率(13.3)而无法复制/移动,如应用于B的相应构造函数会导致从默认的构造函数中删除或无法访问的歧义或函数,即从默认的构造函数中删除或无法访问的
具有析构函数的任何直接或虚拟基类或非静态数据成员,
—对于复制构造函数,是rvalue引用类型的非静态数据成员,或者
—对于移动构造函数,是非静态数据成员,或者具有类型的直接或虚拟基类,该类型不具有move构造函数,并且不是平凡的可复制的。


当前的工作草案确实允许在某些条件下隐式地进行举动,我认为该决议在很大程度上解决了亚伯拉罕的担忧。
James McNellis 2011年

我不确定我知道在Tweak 2和Tweak 3之间的示例中可能会发生什么动作。您能解释一下吗?
Matthieu M.

@Matthieu M .: Tweak 2和Tweak 3都坏了,而且确实非常相似。在Tweak 2中,存在带有不变性的私有成员,这些私有成员可以被移动ctor破坏。在调整3,类没有私有成员本身,而是因为它使用私有继承,公众和基地的保护成员成为派生的私有成员,从而导致同样的问题。
杰里·科芬

1
我真的不明白move构造函数如何破坏int中的类不变式Tweak2。我想这与Number将要移动并vector要复制的事实有关...但是我不确定:/我确实知道问题会逐渐蔓延到Tweak3
Matthieu M.

您提供的链接似乎已死?

8

(到目前为止,我正在处理一个愚蠢的宏...)

是的,我也走那条路。这是您的宏:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(我删除了真实的评论,这些评论的长度和纪实性。)

您可以将类中的基和/或成员指定为预处理器列表,例如:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

随之而来的是移动构造函数和移动赋值运算符。

(顺便说一句,如果有人知道我如何将这些细节组合到一个宏中,那将会很不错。)


非常感谢,我的情况非常相似,除了我必须传递成员变量的数量作为参数(确实很糟糕)。
维克多·塞尔

1
@Viktor:没问题。如果还不算太晚,我想您应该将其他答案之一标记为已接受。我的更多是“顺便说一下,这是一种方法”,而不是您真正问题的答案。
GManNickG 2011年

1
如果我正确地读取了宏,那么只要编译器实现了默认的move成员,上述示例就将不可复制。当存在明确声明的移动成员时,将禁止隐式生成复制成员。
Howard Hinnant

@Howard:没关系,在那之前这是一个临时解决方案。:)
GManNickG 2011年

GMan:如果您具有交换功能,则此宏会添加moveconstructor \ assign:
Viktor

4

VS2010不会这样做,因为它们在实施时还不是标准的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.