Scott Meyers有good viewpoint on the rule of zero。基本上他主张默认移动/复制分配/构造你是否真的需要它们。基本上一般的经验法则是避免编译器生成这些成员,主要是因为它们是混淆的一个重要来源(我同意这一点)。
因此,我正在考虑如何将类定义为默认的可移动,可复制或不可移动,不可复制的通用实践。我想到了提升的boost::noncopyable
,但我不喜欢为这种功能目的引入继承的想法。
我能想到的唯一有意义的是使用宏。所以我想出了类似的东西:
/// Disable copy construct/assign for the given class T
#define CLASS_NON_COPYABLE(T) \
T(T const&) = delete; \
T& operator=(T const&) = delete
/// Disable move construct/assign for the given class T
#define CLASS_NON_MOVABLE(T) \
T(T&&) = delete; \
T& operator=(T&&) = delete
/// Disable both copy and move construct/assign for the given class T
#define CLASS_NON_COPYABLE_OR_MOVABLE(T) \
CLASS_NON_COPYABLE(T); \
CLASS_NON_MOVABLE(T)
/// Default copy move/assign
#define CLASS_DEFAULT_COPYABLE(T) \
T(T const&) = default; \
T& operator=(T const&) = default
/// Default move construct/assign
#define CLASS_DEFAULT_MOVABLE(T) \
T(T&&) = default; \
T& operator=(T&&) = default
/// Defaulted versions of both copy and move construct/assign for the given class T
#define CLASS_DEFAULT_COPYABLE_OR_MOVABLE(T) \
CLASS_DEFAULT_COPYABLE(T); \
CLASS_DEFAULT_MOVABLE(T)
以及如何使用它们的示例:
class foo
{
public:
foo() = default;
virtual ~foo() = default;
CLASS_NON_COPYABLE(foo);
CLASS_DEFAULT_MOVABLE(foo);
};
int main()
{
foo a, b;
a = b; // FAIL: can't copy; class is "non copyable"
a = foo(); // OK: class is 'default movable'
}
对我而言,这看起来比另类更清洁:
class foo
{
public:
foo() = default;
virtual ~foo() = default;
foo(foo const&) = delete;
foo(foo&&) = default;
foo& operator=(foo const&) = delete;
foo& operator=(foo&&) = default;
};
这是有争议的,因为大多数基于风格的问题都是,但我发现前者的好处:
#ifdef
逻辑处理旧编译器上的移动/复制语义,其中C ++ 11不可用(例如CLASS_NON_MOVABLE
将是无操作的。)鉴于我在这里做了很多宏观魔法,我的灵魂有点痛苦,我觉得需要强制性的#SO;帖子,看看我是否有效地这样做而不是疯了"。
所以我有一些紧密耦合的问题:
也许我说这一切都错了。也许这整个问题都是弄巧成拙的,我不是在思考问题。我也愿意重新理解这一切,以防我在兔子洞里走得太远。
答案 0 :(得分:8)
@HowardHinnant has much better advice关于零规则:
class foo
{
public:
// just keep your grubby fingers off of the keyboard
};
答案 1 :(得分:7)
我笑了起来并且赞成了TemplateRex的好答案。话虽这么说,如果你必须声明你的析构函数是虚拟的,那么你不能把所有东西留给编译器。但是,通过了解规则(并假设符合标准的编译器),可以最小化您必须键入的内容以及您必须阅读的内容。
对于这个具体的例子,我建议:
class foo
{
public:
virtual ~foo() = default;
foo() = default;
foo(foo&&) = default;
foo& operator=(foo&&) = default;
};
注意:
如果您声明移动成员,则会隐式删除这两个副本成员。您可以使用此规则来减少锅炉板。
我建议您将数据成员放在班级的顶部,然后直接将您的特殊成员放在其后面。这与许多指南相反,后者建议将您的数据成员置于底部,因为它们对您的读者来说并不重要。但是,如果您的读者想要了解特殊成员在违约时将要做什么,那么读者需要查看您的数据成员。
我建议您始终以相同的顺序订购您声明的特殊会员。这有助于(启动的读者)在不宣布为特殊成员时实现。我有推荐的订单。但无论顺序如何,都要保持一致。
我推荐的顺序是:析构函数,默认构造函数,复制构造函数,复制赋值,移动构造函数,移动赋值。我喜欢这个命令,因为我认为析构函数是最重要的特殊成员。那个函数告诉了我很多关于类设计的内容。我喜欢将我的副本成员组合在一起,并将我的移动成员组合在一起,因为它们通常都是默认的,或者都是删除的。
对于这个例子的复制成员,根据上面的样式指南,很容易(至少对我而言)看到它们被隐式删除,因此阅读量较少(肮脏的手指和所有这些: - 。))
<强>更新强>
冒着脱离主题的风险,foo.cpp最好包含确认您拥有特殊成员的权利:
static_assert(std::is_nothrow_destructible<foo>{},
"foo should be noexcept destructible");
static_assert(std::has_virtual_destructor<foo>{},
"foo should have a virtual destructor");
static_assert(std::is_nothrow_default_constructible<foo>{},
"foo should be noexcept default constructible");
static_assert(!std::is_copy_constructible<foo>{},
"foo should not be copy constructible");
static_assert(!std::is_copy_assignable<foo>{},
"foo should not be copy assignable");
static_assert(std::is_nothrow_move_constructible<foo>{},
"foo should be noexcept move constructible");
static_assert(std::is_nothrow_move_assignable<foo>{},
"foo should be noexcept move assignable");
我在某些地方添加了“nothrow”。如果适用,将其删除,或者将其添加到更多地方(如果适用)。相反,如果适用则使用“平凡”。一种尺寸并不适合所有人。
这种组合在标题中说出你想要的内容,并确认你所说的是你在源代码中得到的内容,非常有利于正确的代码。
在宏下隐藏这个“锅炉板”有一个成本:读者必须查找宏的定义。如果您使用这样的宏,请判断宏的好处是否超过其成本。