9

From some C legacy code I get a number of constants as int *. In the C++ part, I have an enum of underlying type int. Conversion between the enum and int on a single value basis works. However, conversion between int * and enum * is not possible. See code example below.

Why is that and how would I convert a pointer to some int values to a pointer to int enums and vice versa? I kind of expect it to work since the single value conversions work and the underlying types are the same. I read about What happens if you static_cast invalid value to enum class? but could not determine if potentially invalid values play a role here.

int i = 3;
enum E : int;
E e;

e = static_cast<E>(i);   // ok
i = static_cast<int>(e); // ok

int *j;
E * f;

j = static_cast<int *>(&i); // ok
f = static_cast<E *>(&i);   // 'static_cast': cannot convert from 'int *' to 'E *'
j = static_cast<int *>(&e); // 'static_cast': cannot convert from 'E *' to 'int *'

// now use j and f
*j = *f;
NoDataDumpNoContribution
  • 10,591
  • 9
  • 64
  • 104
  • 1
    Have you tried `reinterpret_cast`? – user7860670 Aug 14 '17 at 13:30
  • While you can do the conversion using `reinterpret_cast`, it's rarely useful to do so. You cannot dereference the resulting pointer without invoking undefined behaviour. – davmac Aug 14 '17 at 14:13
  • @davmac Seems like I should either make sure I turn strict aliasing rules off or start thinking about copying. Thanks to the answer and the discussion below the answer I now understand it better. – NoDataDumpNoContribution Aug 14 '17 at 14:16

2 Answers2

8

Why is that?

From the compiler point of view int* and E* are pointers of different non-related types, that is why static_cast is not applicable here.

How would I convert a pointer to some int values to a pointer to int enums and vice versa?

You might try reinterpret_cast instead of static_cast:

f = reinterpret_cast<E *>(&i);
j = reinterpret_cast<int *>(&e);

From reinterpret_cast:

Any pointer to object of type T1 can be converted to pointer to object of another type cv T2

However, note, that dereferencing f or j (i.e. with *f or *j) will be a violation of the strict aliasing rule (for more details see the discussion below). This means that this kind of conversion, though strictly possible, is usually not useful.

davmac
  • 20,150
  • 1
  • 40
  • 68
Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67
  • 3
    It's worth noting that while this "works", it's most likely a strict aliasing violation. –  Aug 14 '17 at 13:34
  • I agree that reinterpret_cast solves the problem. However I wonder why int* and E* are different while int and E aren't. Anyway, will mark as solved. – NoDataDumpNoContribution Aug 14 '17 at 13:37
  • @Trilarion there are some cases when it's legal to use `static_cast`, they are explicitly listed. Formally speaking, your case does not belong to any of these cases, so... you have to use `reinterpret_cast` instead. – Edgar Rokjān Aug 14 '17 at 13:40
  • 2
    @user2079303 It 100% is a strict aliasing violation, here's the proof: https://godbolt.org/g/pekQNX (see the `add eax, eax`. which assumes the two pointers cannot be aliased) –  Aug 14 '17 at 13:46
  • @user2079303 I can find nothing in the standard (at least in N3690) which allows an enum type to be considered a _similar_ type to its underlying type (nor any other allowance for accessing an enum object via underlying type reference). Happy to be shown wrong if you can point out anything relevant in the standard though :) [edit: while I wouldn't consider the GCC behaviour shown just above as 100% proof, I genuinely believe the standard makes no allowance for this form of type-punning] – davmac Aug 14 '17 at 13:51
  • @Frank oh, wow. I stand corrected. At least assuming GCC is correct, which I agree is a safer assumption than mine. – eerorika Aug 14 '17 at 13:52
  • @Frank good to know! I was sure there were a stronger connection between enum and its underlying type. – Edgar Rokjān Aug 14 '17 at 13:58
  • So, does violating strict aliasing mean, that it's not safe to do that conversion? – NoDataDumpNoContribution Aug 14 '17 at 14:00
  • @Trilarion That's kind of a complex subject. My rule of thumb is that strict aliasing violations shouldn't stick around. So `*reinterpret_cast(&val) = 3;` is generally "fine" (from an aliasing standpoint). But keeping that pointer, or passing it around is going to be risky, and will lead to inconsistent behavior between debug and release builds (which is always fun to debug) –  Aug 14 '17 at 14:05
  • @EdgarRokyan your edit is misleading. Using `reinterpret_cast` doesn't itself violate the strict aliasing rule; the violation happens only when you dereference the pointer that you obtained _from_ the `reinterpret_cast` (which isn't shown in your answer, or in the question). – davmac Aug 14 '17 at 14:53
  • @davmac Feel free to edit the answer in order to make more clear! – Edgar Rokjān Aug 14 '17 at 15:11
  • @davmac Added a dereferencing usage in the question to make the violation more clear. Dereferencing was intended. – NoDataDumpNoContribution Aug 14 '17 at 15:30
  • @Trilarion yes, I understood that it was intended - but agreed, it's better to be clear. I just wanted to distinguish the cast itself, which is legal and _rarely but occasionally_ useful (eg. if you know it will later be cast back to the correct type), with the dereference, which yields undefined behaviour. – davmac Aug 14 '17 at 15:36
2

The default 'base type' of an enum is int and can be explicitly specified in the OP. Logically the value stored at E* e where E is an enumeration with base type int is an int. It can't be statically cast.

There's no guarantee in C++ that an enum of base type (say) is layout compatible with short but even if the language tightened up that point there could be issues of type compatibility/

One issue is that E* to int*pi would violate type-safety because pi could be used to quietly set values outside the enumeration. Similarly int* to E* may violate type safety if the integer value isn't in the enumeration.

Note however the standard makes a clear note that there's nothing to preclude an enum taking a value outside its defined set of values:

This set of values is used to define promotion and conversion semantics for the enumeration type. It does not preclude an expression of enumeration type from having a value that falls outside this range.

See here: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf (see note 95 bottom of p. 156)

The only case that could (if layout-compatibility were assured) be valid is E* to const int* because all values of E* are ints and the value cannot be (correctly) modified through the int * pointer without a further violation of the type system.

But I think the language definition is not that subtle.

NoDataDumpNoContribution
  • 10,591
  • 9
  • 64
  • 104
Persixty
  • 8,165
  • 2
  • 13
  • 35
  • 1
    "Logically the value stored at E* e where E is an enumeration with base type int is an int" while (I think) I know what you mean, I think saying it this way could be misleading. There's no formal guarantee that the representation of an enum corresponds to that of its base type (or not that I'm aware, haven't checked exhaustively tbh). Also: "pi could be used to quietly set values outside the enumeration" - doing so via an `int *` would also be a violation of the strict aliasing rule. – davmac Aug 14 '17 at 15:40
  • There's no statement that enumerations are "layout compatible" with their "base type" or "underlying type" (AFAIK). I'm refine the statement. I'm suggesting the standard is unnecessarily restrictive here. Particularly considering the note ' This set of values is used to define promotion and conversion semantics for the enumeration type. It does not preclude an expression of enumeration type from having a value that falls outside this range.'. From what I can see an implementation could use `int` even for an enum with base type `short` for example. – Persixty Aug 14 '17 at 17:27