【问题标题】:Sometimes a good practice to initialize a class pointer member variable to itself?有时将类指针成员变量初始化为自身的好习惯?
【发布时间】:2022-01-02 23:16:50
【问题描述】:

对于一个严格的内部类,不是旨在用作提供给外部客户端的 API 的一部分,初始化类指针有什么本质上的邪恶吗成员变量本身而不是NULLnullptr

请参见下面的代码示例。

#include <iostream>

class Foo
{
public:
  Foo() :
    m_link(this)
  {
  }

  Foo* getLink()
  {
    return m_link;
  }

  void setLink(Foo& rhs)
  {
    m_link = &rhs;
    // Do other things too.
    // Obviously, the name shouldn't be setLink() if the real code is doing multiple things,
    // but this is a code sample.
  }

  void changeState()
  {
    // This is a code sample, but play along and assume there are actual states to change.
    std::cout << "Changing a state." << std::endl;
  }

private:
  Foo* m_link;
};

void doSomething(Foo& foo)
{
  Foo* link = foo.getLink();

  if (link == &foo)
  {
    std::cout << "A is not linked to anything." << std::endl;
  }

  else
  {
    std::cout << "A is linked to something else. Need to change the state on the link." << std::endl;
    link->changeState();
  }
}

int main(int argc, char** argv)
{
  Foo a;
  doSomething(a);

  std::cout << "-------------------" << std::endl;

  // This is a mere code sample.
  // In the real code, I'm fetching B from a container.
  Foo b;
  a.setLink(b);
  doSomething(a);

  return 0;
}

输出

A is not linked to anything.
-------------------
A is linked to something else. Need to change the state on the link.
Changing a state.

优点

将指针变量 Foo::link 初始化为自身的好处是可以避免意外的 NULL 取消引用。由于指针永远不能为 NULL,那么最坏的情况是程序会产生错误输出而不是分段错误。

缺点

但是,这种策略的明显缺点是它似乎是非常规的。大多数程序员习惯于检查 NULL,因此不期望检查与调用指针的对象是否相等。因此,不建议将此技术用于针对外部消费者的代码库,即希望将此代码库用作库的开发人员。

结语

其他人有什么想法吗?有没有其他人在这个主题上发表过任何实质性的言论,尤其是考虑到 C++98 ?请注意,我使用带有以下标志的 GCC 编译器编译了此代码:-std=c++98 -Wall,但没有发现任何问题。

附:请随时编辑这篇文章以改进我在这里使用的任何术语。

编辑

  • 这个问题是本着其他良好实践问题的精神提出的,例如这个关于deleting references 的问题。
  • 提供了一个更广泛的代码示例来消除混淆。具体来说,样本现在是 63 行,比最初的 30 行有所增加。因此,变量名称已更改,因此引用 Foo:p 的 cmets 应适用于 Foo:link

【问题讨论】:

  • “在最坏的情况下,程序会产生错误的输出而不是分段错误”这实际上是有益的吗?阅读快速失败的想法。
  • @user17732522 目的是防止其他程序员,包括我自己,忘记检查 NULL 并随后导致分段错误。这个微服务绝对不会崩溃。错误输出也很糟糕,但总比崩溃好。
  • 假设您没有忘记复制构造函数/赋值运算符,也许只是删除它们。我发现它使用了这种模式:en.wikipedia.org/wiki/…(尽管它不是最好的例子),我隐约记得一些数据结构可以做到这一点。它类似于null object pattern(例如,如果它是一个单链表,你可以拥有size_t length() const { return p == this ? 0 : 1 + p-&gt;length() },而不必担心在nullptr上调用成员函数)
  • @user17732522 假设p 被初始化为NULL。然后每次我需要检索p 时,我都需要检查NULL。如果我忘记检查NULL,我的程序就会崩溃。当然,我会进行单元测试以确保我的程序没有错误,但万一我错过了什么,我真的不希望我的程序崩溃。
  • @Frisky - 我想这取决于您的应用程序域。我曾经在一家银行工作,在那里我们因稍有怀疑不正确的结果而中止了交易。 什么都好 比向客户显示帐户余额不正确要好。 YMMV,等等。

标签: c++ pointers initialization class-design c++98


【解决方案1】:

一开始是个坏主意,但作为空解引用的解决方案是一个可怕的主意。

您不会隐藏 null 取消引用。曾经。空解引用是错误,而不是错误。当错误发生时,程序中的所有不变性都会消失,并且无法保证任何行为。不允许错误立即显现出来并不能使程序在任何意义上都是正确的,它只会造成混淆并使调试变得更加困难。


除此之外,一个指向自身的结构就是一个粗糙的蠕虫罐头。考虑你的文案任务

Foo& operator=(const Foo& rhs) {
    if(this != &rhs)
        return *this;
    if(rhs->m_link != &rhs)
        m_link = this;
    else
        m_link = rhs->m_link;
}

您现在必须检查每次复制时是否指向自己,因为它的价值可能与它自己的身份相关。

事实证明,在很多情况下都需要进行此类检查。 swap应该如何实现?

void swap(Foo& x, Foo& y) noexcept {
    Foo* tx, *ty;
    if(x.m_link == &x)
        tx = &y;
    else
        tx = x.m_link;
    if(y.m_link == &y)
        ty = &x;
    else
        ty = y.m_link;

    x.m_link = ty;
    y.m_link = tx;
}

假设Foo 有某种指针/引用语义,那么你的相等性现在也很重要

bool operator==(const Foo& rhs) const {
    return m_link == rhs.m_link || (m_link == this && rhs.m_link == &rhs);
}

不要指向自己。只是不要。

【讨论】:

  • 感谢您深入了解此答案。我很高兴你提出了复制、交换和平等的话题。在我心里,我知道我想要浅拷贝,但我可以看到其他程序员可能会有不同的想法,并认为他们需要深拷贝。当然,如果是这样的话,这种自我分配将是非常讨厌的。但是当您说:“当错误发生时,您程序中的所有不变性都会消失,并且无法保证任何行为。”时您说服了我。我不认为那是我想要的,所以我将使用 NULL 检查。为你 +1。
  • @FriskySaga 这不是浅拷贝或深拷贝的问题。复制或交换时,空的 Foo 不能变为非空。
【解决方案2】:

Foo 负责自己的状态。尤其是它向用户公开的指针。

如果您以这种方式公开指针,作为公共成员,这是一个非常奇怪的设计决定。在过去的 30 多年里,我的直觉告诉我,这样的指针不是处理 Foo 状态的负责任的方式。

考虑为此指针提供 getter。

Foo* getP() {
    // create a safe pointer for user
    // and indicate an error state. (exceptions might be an alternative)
}

除非您分享更多有关 Foo 是什么的背景信息,否则很难提供建议。

【讨论】:

  • 指针实际上是一个私有成员,将它暴露给公共接口的是getter。但是,为了简洁,我选择不显示吸气剂。事实上,我最初的代码示例没有显示包含,我将整个初始化列表压缩到一行中。但我想我会提高可读性,并将初始化列表扩展到多行。我添加了包含以防有人想复制粘贴我的代码示例并自己运行它。但感谢您的意见并指出这一点。你是对的,不提供吸气剂是不寻常的。
  • 我在前面的评论中用完了字符,但我的意思是说指针是我实际代码中的私有成员。当然,在代码示例中,它是一个公共成员。我可以编辑我的帖子以将其显示为私人成员以消除任何混淆。
  • @FriskySaga 然后我的第一句话适用。
【解决方案3】:

将类指针成员变量初始化为自身而不是 NULLnullptr 有什么本质上的邪恶吗?

没有。但正如您所指出的,根据用例的不同,可能会有不同的考虑因素。

我不确定这在大多数情况下是否相关,但在某些情况下,对象需要保存自己类型的指针,因此它确实与这些情况相关。

例如,单链表中的一个元素将有一个指向下一个元素的指针,因此列表中的最后一个元素通常会有一个 NULL 指针,以表明没有其他元素。所以使用这个例子,结束元素可以改为指向自身而不是 NULL 来表示它是最后一个元素。这真的只是取决于个人的实施偏好。

很多时候,当您过于努力地使其防崩溃时,最终可能会不必要地混淆代码。根据具体情况,您可能会掩盖问题并使问题更难调试。例如,回到单链接的例子,如果使用了指向自身的初始化方法,并且程序中的一个错误试图从列表中的末尾元素访问下一个元素,则列表将返回末尾再次元素。这很可能会导致程序永远“遍历”列表。这可能比简单地让程序崩溃并通过调试工具找到罪魁祸首更难找到/理解。

【讨论】:

  • 我非常喜欢这个答案,我非常感谢您为构建这个答案所付出的努力和时间。对于我的用例,我永远不需要迭代,但现在你让我想知道我是否有可能有一个方法调用链,这些方法调用最终会在没有结束条件的情况下递归地相互调用。实际代码很复杂,包含数千行,所以我将不得不重新考虑是否仍然值得进行防崩溃,但我会在稍后回到这个问题并标记一个已接受的答案。
  • 交叉引用链表对您来说也是个好主意。我在回复@Passer By 时绞尽脑汁,想知道将相同类类型的指针存储为成员变量是否可以接受或司空见惯。但事实上,所有的链表实现都是这样做的。
猜你喜欢
  • 1970-01-01
  • 2021-06-19
  • 1970-01-01
  • 1970-01-01
  • 2016-06-01
  • 2012-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多