为什么在C++11标准的早期草案中没有默认的移动赋值/移动构造函数?

93
我是一个简单的程序员。我的类成员变量通常由POD类型和STL容器组成。因此,我很少需要编写赋值运算符或复制构造函数,因为这些默认已经实现了。
此外,如果我对不可移动的对象使用std::move,它会利用赋值运算符,这意味着std::move是完全安全的。
作为一个简单的程序员,我希望利用移动功能,而不必为我编写的每个类添加移动构造函数/赋值运算符,因为编译器可以简单地将它们实现为"this->member1_ = std::move(other.member1_);..."。
但是它并没有这样做(至少在Visual 2010中),这是有什么特殊原因吗?
更重要的是,有没有办法绕过这个问题?
更新: 如果你看一下GManNickG的答案,他提供了一个很好的宏。而且如果你不知道,如果你实现了移动语义,你可以删除swap成员函数。

5
你知道你可以让编译器生成默认的移动构造函数。 - aaronman
3
std::move 不执行移动操作,它只是将左值转换为右值。移动操作仍然由移动构造函数执行。 - Owen Delahoy
1
你在谈论 MyClass::MyClass(Myclass &&) = default; 吗? - Sandburg
是的,现在是的 :) - Viktor Sehr
4个回答

79

隐式生成移动构造函数和赋值运算符一直是有争议的问题,近期草案中已经进行了重大修订,因此目前可用的编译器在隐式生成方面可能会有不同的行为。

关于该问题的历史,请参见2010 WG21 论文清单并搜索“mov”。

当前的规范(11 月的 N3225 版)规定(N3225 12.8/8):

如果类 X 的定义没有显式声明移动构造函数,那么只有在以下情况下才会被默认地隐式声明:

  • X 没有一个用户声明的拷贝构造函数,并且

  • X 没有一个用户声明的拷贝赋值运算符,并且

  • X 没有一个用户声明的移动赋值运算符,并且

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

  • 移动构造函数不能被隐式定义为删除。

12.8/22 中也有类似的语言,指定了何时隐式声明移动赋值运算符为默认。您可以在N3203:收紧生成隐式移动所需条件中找到完整的更改列表,该文大部分基于 Bjarne Stroustrup 的论文N3201:向右移动提出的解决方案。


4
我写了一篇小文章,其中包含一些图表,用来描述隐式移动构造函数/赋值运算符的关系,链接在这里:http://mmocny.wordpress.com/2010/12/09/implicit-move-wont-go/ - mmocny
1
每当我不得不为了将其指定为虚拟而定义空析构函数在多态基类中时,我还必须显式定义移动构造函数和赋值运算符。这真是太烦人了 :(。 - someguy
@JamesMcNellis:哦,那么“moving”是一个更好的术语,否则其他人可能会再次更改“mov”为“move”,认为这是一个打字错误。 :-) 我更喜欢标题本身的名称:“Moving Right Along”,因为“mov”会找到16个结果;你打算选择全部、其中几个还是仅仅一个? - Nawaz
3
既然C++11已被批准通过,我们是否可以更新这个答案呢?我想知道哪些行为获胜了。 - Joseph Garvin
2
@Guy Avraham:我想我当时说的是(已经过去7年了),如果我有一个用户声明的析构函数(即使是空的虚拟析构函数),那么没有移动构造函数会被隐式地声明为默认值。我想这会导致复制语义?(多年来我都没有接触C ++。)然后James McNellis评论说,virtual ~D() = default;应该可以工作,并仍然允许隐式移动构造函数。 - someguy
显示剩余5条评论

13

隐式生成的移动构造函数曾被考虑加入标准,但可能存在风险。请参阅Dave Abrahams的分析

然而最终标准确实包括了隐式生成移动构造函数和移动赋值运算符,尽管带有相当多的限制:

如果类X的定义没有明确声明移动构造函数,则仅当以下条件全部满足时,会隐式声明为默认:
— X没有用户声明的复制构造函数
— X没有用户声明的复制赋值运算符
— X没有用户声明的移动赋值运算符
— X没有用户声明的析构函数,并且
— 移动构造函数不会被隐式定义为删除的

但事实并不完全如此。ctor可以被声明,但仍定义为删除状态:

一个隐式声明的复制/移动构造函数是其类的内联公共成员。对于类X的默认复制/移动构造函数,如果X具有以下情况,则该函数将被定义为删除状态(8.4.3):
— 具有非平凡对应构造函数的变体成员,并且X是类似于联合体的类
— 非静态数据成员M(或其数组)是类类型,由于重载解析(13.3),无法复制/移动该成员的对应构造函数会导致二义性或被删除或从默认构造函数中不可访问
— 直接或虚拟基类B无法复制/移动,因为重载解析(13.3)作用于B的对应构造函数会导致二义性或被删除或从默认构造函数中不可访问
— 任何具有已删除或从默认构造函数中不可访问的析构函数的直接或虚拟基类或非静态数据成员
— 对于复制构造函数,rvalue引用类型的非静态数据成员;或
— 对于移动构造函数,具有没有移动构造函数且不是平凡可复制的类型的非静态数据成员或直接/虚拟基类。


目前的工作草案确实允许在特定条件下进行隐式移动生成,我认为这个决议在很大程度上解决了阿布拉哈姆的担忧。 - James McNellis
我不确定我理解在Tweak 2和Tweak 3之间的示例中哪个移动可能会出问题。你能解释一下吗? - Matthieu M.
@Matthieu M.:Tweak 2和Tweak 3都有问题,而且问题非常相似。在Tweak 2中,存在具有不变量的私有成员,可以通过移动构造函数来破坏它们。在Tweak 3中,该类本身没有私有成员,但由于使用了私有继承,基类的公共和保护成员成为派生类的私有成员,导致出现相同的问题。 - Jerry Coffin
1
我并不真正理解移动构造函数如何会破坏Tweak2中的类不变量。我想这可能与Number被移动而vector被复制有关...但我不确定:/ 我确实理解问题会级联到Tweak3 - Matthieu M.
你给的链接好像已经失效了? - Wolf
这里有一篇文章:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3153.htm,看起来是相同的分析。 - Ismael

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
1
如果我正确理解您的宏定义,那么一旦编译器实现了默认移动成员,您上面的示例将变为不可复制。当存在显式声明的移动成员时,复制成员的隐式生成被禁止。 - Howard Hinnant
@Howard:没关系,那只是一个临时解决方案。 :) - GManNickG
GMan:如果你有一个交换函数,这个宏会添加移动构造函数和移动赋值运算符: - Viktor Sehr
// 需要:swap成员函数,可用于不可复制的对象 // 实现:移动赋值和移动构造函数 #define 实现可移动()
my_type(my_type&& other) { this->swap(other); }
my_type& operator=(my_type&& other) { this->swap(other); return *this; }
- Viktor Sehr
显示剩余6条评论

5

VS2010无法完成此操作,因为它们在实现时不符合标准。


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