为什么C ++ 11强类型枚举可以通过指针强制转换为底层类型?

时间:2014-07-04 04:08:49

标签: c++ c++11 enums enum-class strongly-typed-enum

在C ++ 11中,我们可以将强类型枚举(enum class)强制转换为其基础类型。但似乎我们无法指出相同的指针:

enum class MyEnum : int {};

int main()
{
  MyEnum me;

  int iv = static_cast<int>(me); // works
  int* ip = static_cast<int*>(&me); // "invalid static_cast"
}

我试图理解为什么会这样:有关于枚举机制的一些内容使得支持这一点变得困难或荒谬吗?这是标准中的简单疏忽吗?还有别的吗?

在我看来,如果枚举类型真正构建在如上所述的整数类型之上,我们应该不仅能够投射值而且还能投射指针。我们仍然可以使用reinterpret_cast<int*>或C风格的演员表,但这比我认为的更重要。

6 个答案:

答案 0 :(得分:13)

TL; DR: C ++的设计者不喜欢打字。

其他人指出为什么标准不允许这样做;我将尝试解决为什么标准的编写者可能会这样做的原因。根据{{​​3}},强类型枚举的主要动机是类型安全。不幸的是,类型安全对许多人来说意味着很多东西假设一致性是标准委员会的另一个目标是公平的,所以让我们在C ++的其他相关背景下检查类型安全性。

C ++类型安全

在C ++中,除非明确指定相关(通过继承),否则类型是不相关的。考虑这个例子:

class A
{
    double x;
    int y;
};

class B
{
    double x;
    int y;
};

void foo(A* a)
{
    B* b = static_cast<B*>(a); //error
}

即使A和B具有完全相同的表示(标准甚至称它们为“标准布局类型”),但如果没有reinterpret_cast,则无法在它们之间进行转换。同样,这也是一个错误:

class C
{
public:
    int x;
};

void foo(C* c)
{
    int* intPtr = static_cast<int*>(c); //error
}

即使我们知道C中唯一的东西是int并且你可以自由访问它,static_cast也会失败。为什么?没有明确指出这些类型是相关的。 C ++旨在支持面向对象的编程,它提供了组合和继承之间的区别。您可以在继承相关的类型之间进行转换,但不能在组合相关的类型之间进行转换。

根据您所看到的行为,很明显强类型枚举通过组合与其基础类型相关联。为什么这可能是标准委员会选择的模型?

组合与继承

有很多关于这个问题的文章写得比我能适合的任何文章更好,但我会尝试总结一下。什么时候使用组合与什么时候使用继承肯定是一个灰色区域,但在这种情况下有很多支持组成的点。

  1. 强类型枚举不能用作整数值。因此,由继承表示的'is-a'关系不合适。
  2. 在最高级别,枚举用于表示一组离散值。通过为每个值分配一个id号来实现这一点的事实通常并不重要(不幸的是C暴露并因此强制执行这种关系)。
  3. 回顾this proposal,允许指定基础类型的列出原因是指定枚举的大小和签名。这更像是一个实现细节,而不是枚举的重要部分,再次支持组合。
  4. 在这种情况下,您可能会争论遗传或构成是否更好,但最终必须做出决定并且行为是以构图为模型。

答案 1 :(得分:8)

相反,以稍微不同的方式看待它。即使static_castlong*具有相同的基本表示,您也不能int* intlong。出于同样的原因,基于int的枚举仍被视为int的唯一且不相关的类型,因此需要reinterpret_cast

答案 2 :(得分:7)

  

枚举是具有命名常量的不同类型(3.9.2)。 [...]每个枚举定义一个与所有其他类型不同的类型。 [...]如果两个枚举类型具有相同的基础类型,则它们是布局兼容的。

[dcl.enum] (§7.2)

底层类型指定内存中枚举的布局,而不是它与类型系统中其他类型的关系(正如标准所说,它是 distinct type ,它自己的一种类型)。指向enum : int {}的指针永远不能隐式转换为int*,就像指向struct { int i; };的指针不能,即使它们在内存中看起来都相同。

那么为什么隐含转换为int首先起作用?

  

对于其基础类型是固定的枚举,其值为   枚举是基础类型的值。 [...] 的价值   枚举器或无范围枚举类型的对象是   通过整数提升(4.5)转换为整数。

[dcl.enum] (§7.2)

因此,我们可以将枚举值分配给int,因为它们的类型为int。由于整数提升的规则,可以将枚举类型的对象分配给int。顺便说一句,这里的标准特别指出这只适用于C风格(无范围)枚举。这意味着您仍然需要示例第一行中的static_cast<int>,但只要您将enum class : int转换为enum : int,它就可以在没有显式强制转换的情况下运行。虽然指针类型仍然没有运气。

整体促销在 [conv.prom] (§4.5)的标准中定义。我将为您提供引用完整部分的详细信息,但这里的重要细节是,其中的所有规则都适用于非指针类型的 prvalues ,因此这些都不适用于我们的小问题。

最后一部分可以在 [expr.static.cast] (§5.2.9)中找到,它描述了static_cast的工作原理。

  

可以显式转换范围枚举类型(7.2)的值   到一个完整的类型。

这解释了为什么您从enum classint的演员表现有效。

但请注意,指针类型允许的所有static_cast(同样,我不会引用相当冗长的部分)需要类型之间的某种关系。如果你还记得答案的开头,那么每个枚举都是一个不同的类型,因此与它们的基础类型或同一基础类型的其他枚举没有任何关系。

这与@MarkB's answer有关:将指针enum静态转换为指向int的指针类似于将指针从一个整数类型转换为另一个整数类型 - 即使两者都具有下面相同的内存布局和一个值将通过规则整体促销隐式转换为另一个,它们仍然是不相关的类型,因此static_cast在这里不起作用。

答案 3 :(得分:6)

我认为思考的错误是

enum class MyEnum : int {};

不是真正的继承。当然,您可以说MyEnum int。但是,它与经典继承不同,因为并非所有int上可用的操作都可用于MyEnum

让我们将其与以下内容进行比较:圆是椭圆。但是,将CirlceShape实现为继承自EllipseShape几乎总是错误的,因为并非椭圆上可能的所有操作都可以用于循环。一个简单的例子是在x方向上缩放形状。

因此,将枚举类视为从整数类型继承会导致您的情况混乱。您不能递增枚举类的实例,但可以增加整数。由于它不是真正的继承,因此禁止静态地向这些类型转换指针是有意义的。以下行不安全:

++*reinterpret_cast<int*>(&me);

这可能是委员会在这种情况下禁止static_cast的原因。一般来说reinterpret_cast被认为是邪恶的,static_cast被认为是好的。

答案 4 :(得分:3)

您可以在标准草案 5.2.9静态演员部分找到您的问题的答案。

支持允许

int iv = static_cast<int>(me);

可以从:

获得
  

5.2.9 / 9范围枚举类型(7.2)的值可以显式转换为整数类型。如果原始值可以由指定的类型表示,则该值不变。否则,结果值未指定。

支持允许

me = static_cast<MyEnum>(100);

可以从:

获得
  

5.2.9 / 10可以将整数或枚举类型的值显式转换为枚举类型。如果原始值在枚举值(7.2)的范围内,则该值不变。否则,结果值未指定(可能不在该范围内)。

支持不允许

int* ip = static_cast<int*>(&me);

可以从:

获得
  

5.2.9 / 11类型为“指向cv1 B的指针”的prvalue,其中B是类类型,可以转换为类型为“指向cv2 D的指针”的prvalue,其中D是派生的类(第10条) )如果存在从“指向D的指针”到“指向B的指针”的有效标准转换(4.10),则cv2与cv1具有相同的cv资格,或者更高的cv资格,并且B既不是虚拟的D的基类也不是D的虚基类的基类。空指针值(4.10)   转换为目标类型的空指针值。如果“指向cv1 B的指针”类型的prvalue指向实际上是D类型对象的子对象的B,则生成的指针指向类型D的封闭对象。否则,转换的结果是未定义的。 / p>

static_cast不能用于将&me投放到int*,因为MyEnumint与继承无关。

答案 5 :(得分:1)

我认为第一个static_cast的原因是能够使用期望旧样式enum的函数和库,或者甚至使用一堆定义的值进行枚举,并直接期望一个整数类型。但是类型enum和整数类型之间没有其他逻辑关系,所以如果你想要那个演员,你应该使用reinterpret_cast。但如果您遇到reinterpret_cast问题,可以使用自己的助手:

template< class EnumT >
typename std::enable_if<
    std::is_enum<EnumT>::value,
    typename std::underlying_type<EnumT>::type*
>::type enum_as_pointer(EnumT& e)
{
    return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}

template< class IntT, class EnumT >
IntT* static_enum_cast(EnumT* e, 
    typename std::enable_if<
        std::is_enum<EnumT>::value &&
        std::is_convertible<
            typename std::underlying_type<EnumT>::type*,
            IntT*
        >::value
    >::type** = nullptr)
{
    return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}

虽然这个答案可能无法让您满意reason of prohibiting static_cast of enum pointers,但它为您提供了一种安全的方式来使用reinterpret_cast