CppCoreGuidelines C.21 是否正确?

Is CppCoreGuidelines C.21 correct?

提问人:alexeykuzmin0 提问时间:7/31/2016 最后编辑:user3840170alexeykuzmin0 更新时间:2/10/2022 访问量:1841

问:

在阅读 Bjarne Stroustrup 的 CoreCppGuidelines 时,我发现了一个与我的经验相矛盾的指南。

C.21 要求满足以下条件:

如果定义或任何默认操作,请定义或全部定义=delete=delete

原因如下:

特殊函数的语义密切相关,因此,如果一个函数需要非默认,那么其他函数也可能需要修改。

根据我的经验,重新定义默认操作的两种最常见的情况如下:

#1:定义具有默认主体的虚拟析构函数以允许继承:

class C1
{
...
    virtual ~C1() = default;
}

#2:默认构造函数的定义,对 RAII 类型成员进行一些初始化:

class C2
{
public:
    int a; float b; std::string c; std::unique_ptr<int> x;

    C2() : a(0), b(1), c("2"), x(std::make_unique<int>(5))
    {}
}

在我的经验中,所有其他情况都很少见。

您如何看待这些例子?它们是 C.21 规则的例外,还是最好在此处定义所有默认操作?还有其他常见的例外情况吗?

C++ C++11 cpp-core-guidelines 五法则

评论

0赞 Richard Critten 7/31/2016
使用 #2,如果您复制或分配类,您希望unique_ptr会发生什么?
0赞 alexeykuzmin0 7/31/2016
我希望该类是不可复制的,并且默认的移动分配工作得很好:旧的被销毁,新的被移动到字段。std::unique_ptrstd::unique_ptr<int>x
0赞 Galik 7/31/2016
可能值得在 github 上将其作为一个问题提出 github.com/isocpp/CppCoreGuidelines/issues
11赞 Bjarne Stroustrup 7/31/2016
回复 (1)。如果定义虚拟析构函数,则希望通过指针和引用来操作该类。在这种情况下,复制通常是一场灾难(切片),因此默认的复制和移动操作几乎肯定是错误的。如果你真的想要这些,请适当地定义它们。
0赞 alexeykuzmin0 7/31/2016
@BjarneStroustrup THX 的解释,现在理解为什么示例 #1 不例外

答:

7赞 NQA 7/31/2016 #1

我认为也许你的第二个例子是一个合理的例外,毕竟,指南确实说“可能性是......”,所以会有一些例外。

我想知道这张幻灯片是否有助于您的第一个示例:

Special Members

以下是幻灯片: https://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

编辑:有关第一种情况的更多信息,我后来发现了这个:C++11虚拟析构函数和移动特殊函数的自动生成

评论

0赞 alexeykuzmin0 7/31/2016
不幸的是,幻灯片没有解释为什么我应该声明复制和移动构造函数和赋值,以防虚拟析构函数具有默认正文。你能更详细地解释一下吗?
1赞 NQA 7/31/2016
@alexeykuzmin0 好吧,如果你不这样做,你就不会有任何移动ctors等。并且副本已被弃用。
0赞 alexeykuzmin0 7/31/2016
对于一个小类来说,缺少移动操作可能不是一个大问题,在声明的析构函数具有默认正文的情况下弃用复制操作的原因对我来说不清楚。幸运的是,BjarneStroustrup在对这个问题的评论中解释了这一点
1赞 NQA 8/1/2016
右。忽略我说的一切!这个人自己刚刚出现...... :)
14赞 Howard Hinnant 8/1/2016 #2

我对这一准则有很大的保留意见。即使知道这是一个指导方针,而不是一个规则我仍然有所保留。

假设您有一个类似于 或 的用户编写的类。它只是一种值类型。它不拥有任何资源,它意味着简单。假设它有一个非特殊成员构造函数。std::complex<double>std::chrono::seconds

class SimpleValue
{
    int value_;
public:
    explicit SimpleValue(int value);
};

好吧,我也想成为默认的可构造的,并且我通过提供另一个构造函数来抑制默认构造函数,因此我需要添加该特殊成员SimpleValue

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

我担心人们会记住这个准则和理由:好吧,既然我提供了一个特殊成员,我应该定义或删除其余的,所以就这样......

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;

    explicit SimpleValue(int value);
};

嗯。。。我不需要移动成员,但我需要盲目地遵循智者告诉我的内容,所以我将删除这些:

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;
    SimpleValue(SimpleValue&&) = delete;
    SimpleValue& operator=(SimpleValue&&) = delete;

    explicit SimpleValue(int value);
};

我担心 CoreCppGuidelines C.21 会导致大量看起来像这样的代码。为什么这样不好?有几个原因:

1.这比这个正确的版本更难阅读:

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

2.坏了。您会发现第一次尝试按值从函数返回 a 时:SimpleValue

SimpleValue
make_SimpleValue(int i)
{
    // do some computations with i
    SimpleValue x{i};
    // do some more computations
    return x;
}

这不会编译。错误消息将说明有关访问 的已删除成员的信息。SimpleValue

我有一些更好的指导方针:

1.了解编译器何时默认或删除特殊成员,以及默认成员将执行哪些操作。

此图表可以帮助解决以下问题:

enter image description here

如果这张图表复杂了,我理解。这很复杂。但是,当一次向您解释一点点时,处理起来就容易多了。我希望能在一周内更新这个答案,并附上我解释这张图表的视频链接。这是解释的链接,经过比我想要的更长的时间(我很抱歉):https://www.youtube.com/watch?v=vLinb2fgkHk

2.当编译器的隐式操作不正确时,始终定义或删除特殊成员。

3.不要依赖于已弃用的行为(上图中的红色框)。如果声明任何析构函数、复制构造函数或复制赋值运算符,则同时声明复制构造函数和复制赋值运算符。

4. 切勿删除移动成员。如果你这样做,充其量是多余的。在最坏的情况下,它会破坏你的类(如上面的例子)。如果你确实删除了移动成员,并且是多余的大小写,那么你就会强迫你的读者不断查看你的类,以确保它不是损坏的大小写。SimpleValue

5.对 6 个特殊成员中的每一个都给予温柔的关怀,即使结果是让编译器为你处理它(也许是通过隐式地抑制或删除它们)。

6.将你的特殊成员按一致的顺序放在你的班级的顶部(只有那些你想明确声明的成员),这样你的读者就不必去搜索他们了。我有我最喜欢的订单,如果你喜欢的订单不同,很好。我的首选顺序是我在示例中使用的顺序。SimpleValue

这是一篇简短的论文,其中对这种类型的类声明有更多理由。

评论

0赞 TemplateRex 8/1/2016
如果您确实需要默认构造函数,为什么不编写 ?如果您需要一个不同于 0 的值,只需写入,然后再次写入。SimpleValue() = default;int value_ = 42;SimpleValue() = default;
1赞 Howard Hinnant 8/1/2016
@TemplateRex:我同意这是实现默认构造函数的好方法,尤其是在示例中。用你的话吹毛求疵:在这个例子中,除非你.它要做的是使行为就像一个标量:将留下一个未指定的值并将零初始化。Fwiw,这正是所做的。我犹豫是否要把所有这些都包括在我的答案中,因为它已经寄宿了太久了。SimpleValueSimpleValue() = default;value_int value_ = 0;SimpleValueSimpleValue x;x.value_SimpleValue x{};std::chrono::duration
0赞 NQA 8/1/2016
考虑到@TemplateRex所说的话,如果我想变得非常挑剔——这完全不是我的事,老实说!:p - 我不能指出 C.21 说“如果您定义或 = 删除任何默认操作......”吗?可以说,在正确的 SimpleValue 示例中,您根本没有定义或 = 删除任何默认操作,这难道不是公平的吗?
0赞 Howard Hinnant 8/1/2016
@JohnPage:问得好。这实际上是我在 C.21 上遇到的另一个问题。它并没有真正具体说明什么含义。我假设“define”的意思是“用户声明的”。并计为“用户声明”。所以现在我们真的没有100%清楚地了解所给出的建议。define=default
0赞 NQA 8/1/2016
“注意:如果你想要一个默认操作的默认实现(同时定义另一个操作),write =default 以表明你是故意为该函数这样做的。”这里有一种暗示,即 和 是“对立面”。但是,是的,我敢肯定它仍然很容易被误解......=defaultdefine