在C++中,对于非连续常量,我应该使用枚举还是多个const?

4
我正在将一组从C语言中的文件IO函数移植到C++类中。这些函数会读取一个文件头,其中有许多特定条目的位置是通过“魔数”(未命名的常量)来表示。
几年前,一位老程序员教我使用“魔数”本质上是不好的,因此,我从那时起尝试避免在我的移植过程中使用未命名的常量。因此,我想创建某种包含条目位置常量列表的东西。
到目前为止,我已经想出了两个相对安全的解决方案——使用命名空间封装的常量集或命名空间封装的枚举。
我能安全地使用其中任何一个解决方案吗?其中一个解决方案是否有优势?
例如:
选项1
namespace hdr_pos {
   const unsigned int item_1_pos=4;
   const unsigned int item_2_pos=8;
   const unsigned int item_3_pos=12;
   const unsigned int item_4_pos=24;
   const unsigned int item_5_pos=32;
};

选项二

namespace hdr_pos {
   enum e {
      item_1_pos=4,
      item_2_pos=8,
      item_3_pos=12,
      item_4_pos=24,
      item_5_pos=32
   };
};

有没有办法防止重复,以便在未来更新文件头时更改位置但忘记更改其中一个时进行捕捉?

请保持客观事实,不带主观情感。如果您不知道任何优点,请随意回答。

注意:当然,在我的实际实现中,我会使用更具描述性的名称;我只是为了举例而将事物称为item_<#>_...


你可以让编译器确保没有重复的枚举值,但这需要一些麻烦并且有点混乱。你可以查看我对另一个问题的回答,了解如何做到这一点 - James McNellis
@James 哇,这真是一团糟……我不知道这丑陋值不值得。我的意思是,如果你搞砸了,文件就无法被正确解析,我想你会很快知道,编译后……不过还是谢谢你,这确实给了我一些关于最后的问题的见解。 - Jason R. Mick
如果你只使用宏来生成代码,那么代码会变得混乱。我编写了一组宏来生成ToString/ToEnum函数、范围检查函数以及枚举类型的流插入和提取运算符;如果你经常使用枚举类型,这样的宏可以非常有用。 - James McNellis
明白了。对于某些项目来说,这听起来是个好主意。但对于这个项目,我想我会避免使用它,不过我完全理解你的意思! - Jason R. Mick
5个回答

2
我认为使用枚举有两个优点。首先,一些调试器可以将常量转换回枚举变量名称(在某些情况下可以使调试更容易)。此外,您可以声明一个枚举类型的变量,它只能保存该枚举中的值。这可以给您提供一种额外的类型检查形式,如果仅使用常量,则不会拥有该形式。
检查值是否重复可能取决于您特定的编译器。最简单的方法可能是编写一个外部脚本,它将解析您的枚举定义并报告值是否重复(如果需要,您可以将其作为构建过程的一部分运行)。

两点建议都很好。我甚至没有想到,但我可能会将索引传递给我的一些模板函数,因此出于你提到的原因,使用枚举似乎是明智的选择... - Jason R. Mick
有一件事我忘了提:如果你需要随时获取这些值中的一个地址,你需要使用常量。除非你将该值分配给临时变量并获取其地址,否则无法创建指向枚举值的指针。 - bta

1

我曾经处理过这种情况,针对错误代码。

我看到有人使用枚举来表示错误代码,但这会带来一些问题:

  1. 你可以将一个不对应任何值的 int 赋给枚举(太糟糕了)
  2. 该值本身在头文件中声明,这意味着错误代码重新分配(这种情况确实发生过...)会破坏代码兼容性,你还必须小心添加元素...
  3. 你必须在同一个头文件中定义所有代码,即使往往某些代码自然限制在应用程序的一小部分中,因为枚举无法“扩展”
  4. 没有检查是否分配了相同的代码
  5. 你无法迭代枚举的各个字段

因此,在设计我的错误代码解决方案时,我选择了另一条路:在源文件中定义的命名空间常量,以解决第 2 和第 3 点。为了增加类型安全性,这些常量不是 int,而是特定的 Code 类:

namespace error { class Code; }

然后我可以定义几个错误文件:

// error/common.hpp

namespace error
{
  extern Code const Unknown;
  extern Code const LostDatabaseConnection;
  extern Code const LostNASConnection;
}

// error/service1.hpp
// error/service2.hpp

虽然我没有解决任意转换问题(构造函数是显式的,但是公共的),因为在我的情况下,我需要转发其他服务器返回的错误代码,我肯定不想知道它们所有的错误代码(那将会太脆弱了)

然而,我确实考虑过这个问题,通过将所需的构造函数设置为私有并强制使用构建器,我们甚至可以一举获得4和5:

// error/code.hpp

namespace error
{
  class Code;

  template <size_t constant> Code const& Make(); // not defined here

  class Code: boost::totally_ordered<Code>
  {
  public:
    Code(): m(0) {} // Default Construction is useful, 0 is therefore invalid

    bool operator<(Code const& rhs) const { return m < rhs.m; }
    bool operator==(Code const& rhs) const { return m == rhs.m; }

  private:
    template <size_t> friend Code const& Make();
    explicit Code(size_t c): m(c) { assert(c && "Code - 0 means invalid"); }

    size_t m;
  };

  std::set<Code> const& Codes();
}


// error/privateheader.hpp (inaccessible to clients)

namespace error
{
  std::set<Code>& PrivateCodes() { static std::set<Code> Set; return Set; }

  std::set<Code> const& Codes() { return PrivateCodes(); }

  template <size_t constant>
  Code const& Make()
  {
    static std::pair< std::set<Code>::iterator, bool > r
      = PrivateCodes().insert(Code(constant));
    assert(r.second && "Make - same code redeclared");
    return *(r.first);
  }
}

//
// We use a macro trick to create a function whose name depends
// on the code therefore, if the same value is assigned twice, the
// linker should complain about two functions having the same name
// at the condition that both are located into the same namespace
//
#define MAKE_NEW_ERROR_CODE(name, value)         \
  Make<value>(); void _make_new_code_##value ();


// error/common.cpp

#include "error/common.hpp"

#include "privateheader.hpp"

namespace error
{
  Code const Unkown = MAKE_NEW_ERROR_CODE(1)
  /// ....
}

稍微多做一点工作(对于框架),并且只进行链接时/运行时检查相同的赋值检查。虽然通过扫描模式MAKE_NEW_ERROR_CODE很容易诊断重复。
玩得开心!

0

你的问题标题表明,你对使用枚举类型存在疑虑的主要原因是你的常量是非迭代的。但在C++中,枚举类型已经是非迭代的了。你需要跳过很多障碍才能创建一个迭代的枚举类型。

我认为,如果你的常量本质上是相关的,那么使用枚举类型是一个相当不错的想法,无论这些常量是否是迭代的。然而,枚举类型的主要缺点是完全缺乏类型控制。在许多情况下,你可能更喜欢严格控制常量值的类型(比如将它们设置为无符号),而这是枚举类型无法帮助你实现的(至少目前还不能)。


我正在将标题更改为顺序的。我的意思是,通常我看到枚举示例用于数字的连续字符串,而不是像我这里一样的不连续递增的字符串... - Jason R. Mick

0
需要记住的一件事是,您不能获取枚举类型的地址: const unsigned* my_arbitrary_item = &item_1_pos;

0
如果它们是纯常量并且不需要运行时的东西(比如不能使用非枚举值初始化枚举),那么它们应该只是const unsigned ints。当然,枚举更少打字,但这不是重点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接