美文网首页
听过C语言有规则,听过C ++的零规则吗?

听过C语言有规则,听过C ++的零规则吗?

作者: Python编程导师 | 来源:发表于2019-03-02 20:56 被阅读3次

你们中的一些人可能已经知道零度规则。你们中的更多人可能听说过3号规则(前C ++ 11)或5号规则。让我们首先回顾一下“零规则”是什么。小编c++学习群825414254获取c++一整套系统性的学习资料还有数十套pdf

什么是零规则?

它背后的想法如下:类不应定义任何特殊函数(复制/移动构造函数/赋值和析构函数),除非它们是专用于资源管理的类。它在描述博客文章。这有几个很好的理由,一个更具概念性,另一个更实用。概念上的原因源于单一责任原则,即一个阶级应该对一件事负责的想法。如果你让一个类负责多个事情,那么你已经将两个独立事物的实现和接口紧密耦合在一起。你的类的工作是协调其成员变量的状态,以提供一些组合状态,以及与该状态的接口。当你编写特殊的成员函数时,你基本上就会收集那些没有以你想要的方式管理资源的成员留下的东西。正确的做法是确保您使用正确的成员。这直接导致了我们的实际原因:在c ++中,一个类不能选择它将重新定义特殊功能的成员。我的意思是什么?好吧,如果你不编写特殊函数,编译器会通过尝试逐个成员地应用预期的操作来为你生成它们。因此,如果使用默认的移动构造函数,编译器将生成一个移动构造函数,它只是尝试移动构造所有数据成员(和基类)。只要编写一个自定义移动构造函数,对一个数据成员进行一些特殊处理,您就必须编写代码来处理其他所有成员,即使您只需要更改一个。毕竟,根据自定义特殊功能,每个变量的一行代码是什么似乎不是什么大问题?真正的问题不是必须编写它,因为编译器不会强制执行它并且很容易忘记。

<span style="color:#555555">class Example {  public:  Example(const Example &amp; other) : m_double(other.m_double) { }  private:  double m_double = 0.0;  bool m_bool = false; }; </span>

您不会在IDE或任何清理程序中收到此警告,因为m_bool将通过其内联初始化确定性地设置为false,并且在构造函数初始化列表中未提及它。但这几乎肯定是一个错误,因为无论何时复制一个Example对象,无论副本的来源如何,新副本都将具有m_bool = false。像这样的东西很容易引入,之后可能很难挖掘。

为什么人们违反规则

当然,人们通常不会像上面的例子那样编写代码,因为双精度和布尔值很简单,并且几乎可以保证你对它们的默认资源语义感到满意。让我们来看一个非常常见的真实案例:指针。一个类有指针是很常见的。当然,我们是精明的现代c ++ 11程序员,所以我们将使用unique_ptr,防止任何可能的泄漏。问题是,尽管unique_ptr提供了正确的析构函数,但除非我们希望拥有unique_ptr的类是不可复制的,否则它不会执行我们想要的复制构造函数/赋值。让我们假设Example想要通过指针拥有一个非多态类Pointee,我们最终得到的代码如下所示:

<span style="color:#555555">class Example {  public:  Example(const Example &amp; other)      : m_pointer(make_unique&lt;Pointee&gt;(*other.m_pointer))      , m_bool(other.m_bool)  { }    // Won't be default generated unless you add this  Example(Example &amp;&amp;) = default;    // similar code for copy/move assignment  private:  unique_ptr&lt;Pointee&gt; m_pointer;  bool m_bool = false; }; </span>

沿着这些方向的代码在野外很常见,并且它不是最佳的。在更大的类中,添加或删除变量的每个修改都很容易在复制/移动语义中引入错误。我们可以做得更好吗?

如何遵守规则

上面代码的结论是unique_ptr实际上并不代表我们类的正确资源管理。因此,我们不应该将这个问题作为整个Example类的问题,包括其他所有成员,而应该在根目录中解决问题。让我们编写一个具有适当资源语义的类。

<span style="color:#555555">template&lt;class T&gt; class DeepCopyPointer {  public:  DeepCopyPointer(const DeepCopyPointer &amp; other)      : m_pointer(make_unique&lt;T&gt;(*other.m_pointer))  { }    DeepCopyPointer(DeepCopyPointer &amp;&amp;) = default;    // similar code for copy/move assignment  private:  unique_ptr&lt;T&gt; m_pointer; }; </span>

这看起来更好,对吧?嗯,有一个问题:这个类实际上并没有提供unique_ptr所做的任何接口。我们可以让Example成为朋友并让它直接访问unique_ptr,但这很难看。另一个问题是这个代码非常专业,它只适用于unique_ptr,它只适用于非多态对象:如果我们存储基类指针并且对象的类型不同,则此副本将无法正常工作。我们可能想要通过调用克隆方法或类似方法进行复制。

打破另一条规则是更好的解决方案

让我们做一些更通用的事情。让我们编写一个模仿另一个类的类,但允许您更改其复制行为。为了模仿其他类,我们将继承它。现在,我们有兴趣模仿unique_ptr,继承它是不是很难?人们经常说你不应该继承像unique_ptr或vector这样的类型,因为它们没有虚函数,特别是析构函数。答案是,只要你从不以多态方式使用它,继承就不错了。考虑到这一点:

<span style="color:#555555">template &lt;class T, class F&gt; class copyer : public T {  public:  copyer(T &amp;&amp; t)      : T(std::forward(t)) { };    copyer(copyer &amp;&amp; other) = default;  copyer &amp; operator=(copyer &amp;&amp; other) = default;    copyer(const copyer &amp; other)      : T(F()(other)) { }    copyer &amp; operator=(const copyer &amp; other) {    std::swap(*this, copyer(other));    return *this;  } }; </span>

T是要模仿的类,F是一个有助于我们重新定义复制行为的函子。请注意,我们将使用移动构造/赋值的默认值,我们假设我们喜欢T的移动构造/赋值的默认值。我选择这样做是因为如果T不可移动(或者不能以我们喜欢的方式移动),那么一般来说编写这段代码会相当棘手。要获得我们想要的深层副本unique_ptr,我们只需执行以下操作:

<span style="color:#555555">template &lt;class T&gt; struct F {    std::unique_ptr&lt;T&gt; operator()(const std::unique_ptr&lt;T&gt; &amp; other) {    return std::make_unique&lt;T&gt;(*other);  } };  template &lt;class T&gt; using copying_ptr = copyer&lt;std::unique_ptr&lt;T&gt;, F&lt;T&gt;&gt;; </span>

我们现在可以像这样编写示例:

<span style="color:#555555">class Example {  private:  copying_ptr&lt;Pointee&gt; m_pointer;  bool m_bool = false; }; </span>

就是这样,我们现在免费获得所有特殊功能。我们不必担心每次添加/删除变量时都会向Example添加错误。我们也可以轻松地重用copyer,如果Example想要以不同的方式改变复制语义,它可以只定义一个私有嵌套结构并在模板复制器中使用它而不是F.

综上所述

不要为复杂的类开始编写大的特殊函数,只是因为有几个成员没有正确的默认行为。使用像这样的技术来代替具有默认行为的成员; 您的代码将更清晰,更不容易出错。编辑:这是关于异常安全的简要跟进,受到Ross Smith(CaptainCrowbar)对cpp reddit的一些优秀评论的启发。

强有力的例外保证

有一个地方默认生成的特殊功能不足。强大的异常保证说如果函数通过抛出失败,那么程序的状态与调用函数之前的状态相同。对于构造函数,如果每个成员都有适当的析构函数,则会自动维护; 构造函数体中抛出的异常意味着将销毁部分构造的对象。移动分配是移动,对于具有noexcept移动操作的类型来说,它是非常常见和可取的。所以复制任务是奇怪的人。默认的复制分配将尝试按顺序复制分配每个成员。例如:

<span style="color:#555555">class Example {  private:  std::vector m_vec1;  std::vector m_vec2; };  Example e1; Example e2; .... e2 = e1; </span>

这里可能发生的是e1.m_vec1将被复制到e2.m_vec1中,然后将e1.m_vec2的副本复制到e2.m_vec2中。现在,e2处于半复制状态,强大的异常保证被破坏。所以这是我们如何解决它:

<span style="color:#555555">class Example {  public:  Example(const Example &amp;) = default;  Example(Example &amp;&amp;) = default;  Example &amp; operator=(Example &amp;&amp;) = default;    Example &amp; operator=(const Example &amp; other ) {    std::swap(*this, Example(other));    return *this;  }  private:  std::vector m_vec1;  std::vector m_vec2; }; </span>

我只是使用std :: swap而不是using namespace std技术,因为我知道我不会写自定义交换; 在绝大多数情况下,没有必要。现在,如果抛出异常,它将在执行交换之前在Example(其他)中构造临时值时抛出,并且e2将保持不变。请注意,std :: swap调用移动构造函数并移动赋值,因此三个默认的特殊函数都用于生成提供强保证的复制赋值。那么,这是否符合零规则的精神?它似乎不是,因为我们明确写了一个特殊的功能。但我认为确实如此。我强调的主要是让你的班级处理其成员的资源管理细节是不可取的。在这里,这不会发生。请注意,添加或删除成员时,不需要更改任何特殊功能代码,前提是它们是提供正确语义的功能代码。也许拿走就是这样:Zero规则中的真正零点是你的特殊函数应该引用你的成员变量的零。无论是源于不写入,明确默认,还是复制和交换,所有这些方法都很好。

相关文章

网友评论

      本文标题:听过C语言有规则,听过C ++的零规则吗?

      本文链接:https://www.haomeiwen.com/subject/swiyuqtx.html