在 C++ 中使用枚举而不是结构进行标记分派

Using enum instead of struct for tag dispatching in C++

让我们从标准库中实现 std::unique_lock

struct defer_lock_t { explicit defer_lock_t() = default; };
struct try_to_lock_t { explicit try_to_lock_t() = default; };
struct adopt_lock_t { explicit adopt_lock_t() = default; };

inline constexpr defer_lock_t  defer_lock {};
inline constexpr try_to_lock_t try_to_lock {};
inline constexpr adopt_lock_t  adopt_lock {};

unique_lock (mutex_type& m, defer_lock_t t) noexcept;
unique_lock (mutex_type& m, try_to_lock_t t);
unique_lock (mutex_type& m, adopt_lock_t t);

为什么人们不会't/couldn't/shouldn不使用枚举而不是结构来实现标签分发?如:

enum defer_lock_t { defer_lock };
enum try_to_lock_t { try_to_lock };
enum adopt_lock_t { adopt_lock };

unique_lock (mutex_type& m, defer_lock_t t) noexcept;
unique_lock (mutex_type& m, try_to_lock_t t);
unique_lock (mutex_type& m, adopt_lock_t t);

后者更简洁

我能想到的使用结构的唯一优点是继承(例如,迭代器标记)。但在所有其他情况下,为什么不使用枚举?

首先,您不希望标签类型是 {} 可构造的,您希望明确命名它们。这并不特别适用于 unique_lock 因为 unique_lock<std::mutex> lk(m, {}) 会模棱两可,但有一个一般原则。标记类型的设计使得您必须编写 std::defer_lock (或者,如果您真的想要, std::defer_lock_t()).

其次,您真的只想在预期使用标签类型的特定上下文中使用标签类型。如果你让它们成为 enums,那么你就引入了所有的 enum 功能——比如可以转换为整数:

std::make_unique<int>(std::defer_lock); // ok?

// is this like super deferred?
auto x = std::defer_lock * 2;

// what do you get when you multiply six by nine?
std::unique_lock lk(m, static_cast<std::defer_lock_t>(42));

这些其他的表达方式毫无意义,所以最好不要让它们存在。

第三,在只有少量固定字符的情况下实现标准库的简洁性并不是一个大问题。所以我什至不认为 enum 实施是成功的。标准库中没有 种标记类型。

除了 Barry 列出的原因之外的另一个(次要)好处是,如果函数调用未内联,则枚举标记具有需要传递到函数中的状态,而结构标记则不需要。即使看起来枚举是空的,无作用域枚举总是至少有一个字节的状态可以转换到它们中,而有作用域的枚举总是有至少一位状态。参见 http://eel.is/c++draft/enum#dcl.enum-7

给定

struct s {};
enum e {};

void a(s);
void b(e);

void c() {
    a(s());
}

void d() {
    b(e());
}

64 位 Linux 的 clang 和 gcc 都生成

c():                                  # @c()
        jmp     a(s)                          # TAILCALL
d():                                  # @d()
        xor     edi, edi
        jmp     b(e)                          # TAILCALL

但请注意,在 Windows 上,调用约定似乎阻止了这一点(MSVC 代码生成):

$T1 = 8
void c(void) PROC                                      ; c, COMDAT
        movzx   ecx, BYTE PTR $T1[rsp]
        jmp     void a(s)                   ; a
void c(void) ENDP                                      ; c

void d(void) PROC                                      ; d, COMDAT
        xor     ecx, ecx
        jmp     void b(e)                            ; b
void d(void) ENDP                                      ; d

现场观看:https://godbolt.org/z/ss7Ke64ca