jeudi 3 août 2017

Can I always safely cast into the underlying type of a fixed (scoped) enumeration?

TL;DR: Is the following always safe? Or does it lead to undefined, unspecified or implementation defined behaviour?

template <class T> 
using ut = typename std::underlying_type<T>::type;

template <typename E> ut<E> identity(ut<E> value) {
  return static_cast<ut<E>>(static_cast<E>(value));
}


If I have a scoped enumeration I can always cast it into the underlying type:

#include <cassert>             // if you want to follow along
#include <initializer_list>    // copy everything and remove my text

enum class priority : int { 
  low = 0, 
  normal = 1,
  high = 2 
};

// works fine
int example = static_cast<int>(priority::high);

For all values that are defined in the enumeration I can also expect that I get the value back:

constexpr priority identity_witness(priority p) {
  return static_cast<priority>(static_cast<int>(p));
}

void test_enum() {
  for (const auto p : {priority::low, priority::normal, priority::high}) {
    assert(p == identity_witness(p));
  }
}

According to N3337 (C++11), 5.2.9 Static cast [expr.static.cast] § 9-10 this is fine:

  1. A value of a scoped enumeration type (7.2) can be explicitly converted to an integral type. The value is unchanged if the original value can be represented by the specified type. …
  2. A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). …

However, I'm interested in the other way round. What happens if I cast to an enum and back to the underlying type?

constexpr int identity_witness(int i) {
  return static_cast<int>(static_cast<priority>(i));
}

void test_int() {
  for (const auto p : {0, 1, 2, 3, 4, 5}) {
    assert(p == identity_witness(p));
  }
}

int main() {
  test_enum();
  test_int();
}

This compiles and works fine, since a static_cast to the underlying type won't change the memory at all (probably). However, the standard says that the behaviour is unspecified if the value is not in the range:

  1. [continued] Otherwise, the resulting value is unspecified (and might not be in that range).

The range of the enumerations isn't clear to me. According to 7.2§7, "the values of the enumeration are the values of the underlying type" if the enumeration's underlying type is fixed. Therefore, for any std::underlying_type<my_enumeration_type>, the property above should hold.

Does this argument hold, or did I miss some strange clause in the standard so that a cast into the underlying type of the enumeration might lead to undefined or unspecified behaviour?

Aucun commentaire:

Enregistrer un commentaire