【问题标题】:Are type fields pure evil?类型字段是纯粹的邪恶吗?
【发布时间】:2011-04-18 00:25:30
【问题描述】:

正如在第 12.2.5 节中的 The c++ Programming Language 3rd Edition 中所讨论的那样,与使用虚函数和多态性的等效代码相比,类型字段往往会创建通用性较差、容易出错、直观性和可维护性较差的代码。

作为一个简短的示例,以下是类型字段的使用方式:

void print(const Shape &s)
{
  switch(s.type)
  {
  case Shape::TRIANGE:
    cout << "Triangle" << endl;
  case Shape::SQUARE:
    cout << "Square" << endl;
  default:
    cout << "None" << endl;
  }
}

显然,这是一场噩梦,因为向其中添加一种新的形状以及十几个类似的功能会容易出错且费力。

尽管存在这些缺点以及 TC++PL 中描述的那些缺点,但是否有任何示例表明这种实现(使用类型字段)比利用虚函数的语言特性更好? 或者这种做法应该被列为纯粹的邪恶吗?

现实的例子比做作的例子更受欢迎,但我仍然对做作的例子感兴趣。另外,你有没有在生产代码中看到过这种情况(即使虚函数会更容易)?

【问题讨论】:

  • 在惯用的 C++ 中,您将通过引用而不是指针传递形状。事实上,您很可能会使用 const 引用。
  • 我见过这种事情是在老 c 程序员编写的代码中完成的。我怀疑他们不知道更好的方法,或者不想学习。
  • -1:没有人应该写任何语言功能是“纯粹的邪恶”
  • @Jay:这不是“语言功能”,它是多种语言功能的潜在应用。在任何图灵完备的语言中,你肯定可以做一些符合“纯粹邪恶”的事情。
  • @Jay:好吧,那是一个不纯的邪恶。 :-)

标签: c++ code-organization


【解决方案1】:

我不知道任何现实的例子。人为的方法取决于不能使用虚拟方法的某些充分理由。

【讨论】:

  • 为了记录,我并不是说它是纯粹的邪恶,只是它是前 OO。
  • 我很高兴看到反对票的解释,但我并没有屏住呼吸。我相信我只是在虚张声势,而你什么都没有。
  • 我不知道谁对你投了反对票,但因为你的回答似乎说没有真实的例子(你的第二句话暗示任何例子都必然是人为的),而其他答案肯定会显示其他情况(例如结构通过任何形式的 IPC),所以我可以看到有人会认为这是一个糟糕的答案。
  • @Ben:这样的人在他们的读写能力上会有缺陷,正如我明确指出的那样,任何反例都必须依赖于环境的存在,例如共享内存,这会阻止虚拟方法被用过。
  • 而且,需要明确的是,我确实认为“通过任何形式的 IPC 传递的结构”是人为的,因为 IPC/RPC 需要某种形式的序列化。设计在于要求序列化形式可以按原样执行;一个我从未见过的需求,除了那些试图过度优化的人自己生成的。
【解决方案2】:

当您“知道”您有一组非常具体的、少量的、恒定的类型时,像这样对它们进行硬编码会更容易。当然,常量不是,变量不是,所以在某些时候你可能不得不重写整个东西。

这或多或少是Alexandrescu's articles 中的几个用于区分联合的技术。

例如,如果我正在实现 JSON 库,我会知道每个值只能是对象、数组、字符串、整数、布尔值或 Null — 规范不允许任何其他值。

【讨论】:

  • +1:一个小而固定的类型集的明显实例是不太可能快速改变的外部标准(例如,ISO 标准需要相当长的时间才能改变,即使充其量也是如此)。跨度>
  • 好吧,我看了一篇文章,但我没有看到任何东西可以说服我在 C++ 中使用 Pascal 风格的可区分联合,而不是仅仅通过虚拟方法利用多态性。我错过了什么?
  • 我写了一个 JSON 库,尽管 JSON 级别的对象数量有限,但我可以很方便地实现一种以上类型的 Object,这意味着子类化。 (一种类型使用字典,另一种使用 DTO 上的反射。)同样,也许我遗漏了一些东西,但这种技术的论点似乎没有给人留下深刻印象。
  • @Steven:当然,您可以使用多态性。以上可以让您更严格地控​​制分配(例如,您可以通过小对象优化来节省空间),并且在某些情况下可能会更快。如果上面不清楚,我的 Value 类型以动态语言类型的方式像任何其他类型一样工作,并且有区别的联合允许 Value 对象动态更改其自己的类型。但是,是的,Array 仍然可以为 vector 和 Object 模型 map 建模。
  • @Steven:我已经有一段时间没有读过那些文章了(我仍然在某处有死树杂志;),所以我不记得他是否涵盖了动机或者只是假设并涵盖实现。
【解决方案3】:

难道没有与虚函数和多态性相关的成本吗?就像为每个类维护一个 vtable,每个类对象大小增加 4 个字节,运行时缓慢(尽管我从未测量过)以适当地解析虚函数。因此,对于简单的情况,使用type 字段似乎是可以接受的。

【讨论】:

  • 指向 vtable 的指针的成本被持有类型枚举的成本所抵消。同样,我们不是在 switch/case 中检查每种类型,而是直接跳转到表偏移量。我不相信在这里使用类型技巧可以提高速度。
  • 有成本,是的,但是类型字段有一些类似的成本。类型成员占用一些空间;选择要执行的正确代码的 switch 语句与 vtable 查找一样需要一些时间。我也从未测量过,但如果有利于类型字段的显着差异,我会感到惊讶。
  • 检查枚举类型与动态调用之间的区别通常是微不足道的。但是,静态检查会进行静态调用,这可以内联以节省大量成本。
【解决方案4】:

类型枚举可以通过 memcpy 序列化,而 v-table 不能。一个类似的特性是类型枚举值的损坏很容易处理,v-table 指针的损坏意味着即时错误。甚至没有可移植的方法来测试 v-table 指针的有效性,调用 dynamic_casttypeinfo 对无效对象进行 RTTI 检查是未定义的行为。

例如,当通过 Windows 消息队列传递指向结构的指针时,我选择使用由鉴别器控制的静态分派而不是动态分派的类型层次结构。这为我提供了一些保护,以防止其他可能从我正在使用的范围内分配广播消息的软件(它应该为应用程序本地消息保留,如果您认为该规则实际上得到遵守,请不要通过 GO)。

【讨论】:

  • 如果有什么东西随机破坏了内存,我认为无论如何你都会被淹没。实际上,v-table 可以序列化,因为您只需添加另一个方法(或搭载 RTTI),该方法返回一个枚举,指示类型或对象需要如何反序列化。或者将其构建到虚拟序列化方法中。
  • @Roger:如果您考虑随机损坏,您还没有完全理解我的消息传递示例。 memcpy 和调用虚拟 serialize 方法之间的区别似乎也让你无法理解。让我用一个例子把它们联系在一起:一个 shm 文件。无法保证 v-table 存储在共享文件的所有进程中的相同地址,因此 v-table 指针将不起作用。您需要不断地序列化和反序列化。如果用户不小心覆盖了文件,使用 v-table 指针就无法检测和报告问题。
  • 嗯,只要我们在同一个进程中,memcpy 应该可以正常工作,因为实例只包含指向 vtable 的指针,而不是表本身。指针在被复制后同样有效。如果您的意思是对文件进行完全序列化,那么也有相应的技术。
  • 我知道人们正在阅读我的答案的第一句话然后停下来......是的,有一些技术可以在序列化期间重建对象的动态类型(意思是 vfptr),但它们非常与仅将位存储为一个块相比昂贵,并且对于与共享内存一起使用来说太昂贵了。 (它们通常也颠覆了类型系统,在不首先调用构造函数的情况下创建对象。)
  • @Steven:你不能 memcpy 非 POD 类型;那是UB。
【解决方案5】:

我认为如果类型与隐含的类完全对应,那么类型就是错误的。它变得复杂的地方是类型不完全匹配或者它没有那么切割和干燥。

以你的例子为例,如果类型是红色、绿色、蓝色。这些是形状的类型。你甚至可以将颜色类作为 mixin;但它可能太多了。

【讨论】:

    【解决方案6】:

    我正在考虑使用类型字段来解决向量切片的问题。也就是说,我想要一个分层对象的向量。例如,我希望我的向量是一个形状向量,但我想存储圆形、矩形、三角形等。

    由于切片,您无法以最简单的方式做到这一点。因此,正常的解决方案是使用指针向量或智能指针。但我认为在某些情况下使用类型字段将是一种更简单的解决方案(避免新/删除或替代生命周期技术)。

    【讨论】:

      【解决方案7】:

      以下指南来自 Robert C. Martin 的 Clean Code。 “我对 switch 语句的一般规则是,如果它们只出现一次,用于创建多态对象,并且隐藏在继承关系后面以便系统的其余部分看不到它们,则它们是可以容忍的。”

      基本原理是:如果您将类型字段公开给其余代码,您将获得上述 switch 语句的多个实例。这明显违反了 DRY。当您添加一个类型时,所有这些开关都需要更改(或者更糟糕的是,它们会变得不一致而不会破坏您的构建)。

      【讨论】:

        【解决方案8】:

        我能想到的最好的例子(也是我之前遇到的例子)是当你的类型集是固定的并且你想要执行的函数集(取决于这些类型)是流动的。这样,当您添加一个新函数时,您只需要修改一个位置(添加一个开关),而不是添加一个新的基本虚函数,而真正的实现分散在类型层次结构中的所有类中。

        【讨论】:

          【解决方案9】:

          我的看法是:视情况而定。

          参数化的Factory Method design pattern 依赖于这种技术。

          class Creator {
              public:
                  virtual Product* Create(ProductId);
          };
          
          Product* Creator::Create (ProductId id) {
                  if (id == MINE)  return new MyProduct;
                  if (id == YOURS) return new YourProduct;
                  // repeat for remaining products...
          
                  return 0;
          }
          

          那么,这很糟糕吗?我不这么认为,因为现阶段我们没有其他选择。这是一个绝对必要的地方,因为它涉及到对象的创建。对象的类型尚不清楚。

          然而,OP 中的示例确实需要重构。在这里,我们已经在处理现有的对象/类型(作为参数传递给函数)。

          正如Herb Sutter 提到的那样——

          “关闭:避免打开 要自定义的对象的类型 使用模板和虚拟 让类型(不是他们的 调用代码)决定他们的行为。”

          【讨论】:

          • 这不是我的意思。在这里,您仍然在创建 MyProduct 或 YourProduct。两个不同的班级。我的问题将涉及始终创建一个“OurProduct”,但该产品的类型 in 确定它的行为类似于 MyProduct 还是 YourProduct。实际的类 MyProduct 和 YourProduct 将不存在。我的示例中的 Shape 类就是如此。没有三角形类。只有一个 Shape 类型为 == TRIANGLE。
          • "绝对必要的地方"...不是这样...对象可以注册它们的 id 和创建函数,或者您可以连续调用已注册的“如果你可以”构造函数(阅读关于 GoF 中的创建模式),因此不需要上面显示的那种集中式切换。并不是说对于更本地化的代码库来说,它仍然不是一个更简单且完全可以接受的解决方案。
          猜你喜欢
          • 2011-01-02
          • 1970-01-01
          • 2011-12-07
          • 1970-01-01
          • 1970-01-01
          • 2017-03-13
          • 2011-03-21
          • 1970-01-01
          • 2016-11-18
          相关资源
          最近更新 更多