1

Consider the following two pieces of code:

template <int X>
struct Foo
{
  enum
  {
    x = X
  };
};

vs

template <int X>
struct Foo
{
  static constexpr int x = X;
};

(The former is a frequent pattern in a library I want to modernize to C++17.)

I have looked through many questions/answers on here covering the differences between the enum vs static constexpr variants, but with C++17 having changed the behavior of static constexpr (by making such variables implicitly inline) many of them are outdated (example). It's not perfectly clear to me what the remaining differences are and if I am missing something important.

Is going from the first snippet above to the second a safe transformation? Or are there any potential breakages or changes in behavior affecting user code that I should be aware of?

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • It is valid to initialize a constexpr value with in inline initializer: 'static constexpr int x {X};' No need to initialize it outside of the class. – thelizardking34 Apr 30 '21 at 13:55
  • 1
    Since I was the one answering the question linked, I am happy to announce that with C++17 my answer no longer applies! Hooray! – SergeyA Apr 30 '21 at 14:06
  • 1
    I wonder if this question would benefit of a language-lawyer tag, given some possible subtleties. – Bob__ Apr 30 '21 at 14:07
  • 1
    There could be some obscure SFINAE use cases which could tell the difference. Example : https://godbolt.org/z/5excjfM3z – François Andrieux Apr 30 '21 at 14:16

2 Answers2

3

It is incorrect to assume that the underlying type of the unscoped enum in the original struct is actually an int. As declared, the following applies:

Declares an unscoped enumeration type whose underlying type is not fixed (in this case, the underlying type is an implementation-defined integral type that can represent all enumerator values; this type is not larger than int unless the value of an enumerator cannot fit in an int or unsigned int

see: https://en.cppreference.com/w/cpp/language/enum

The type of the enum is actually undefined. Unscoped enums implicitly convert to integral types (and from there, can be converted to floating-point types) in calling code. It could also mean different compilers will create a different underlying type for the enum. By default; these two types are not interchangeable.

thelizardking34
  • 338
  • 1
  • 12
  • Poorly worded, edited to reflect better explanation – thelizardking34 Apr 30 '21 at 14:32
  • Yes, this is a good point. That said, since the type is not guaranteed in the first place, picking a particular one yourself (instead of leaving it to the compiler) shouldn't cause complications. And if user code somehow relies on the specific type the compiler chose, it's possible to just use that particular type in the new code. Definitely something to think through though! – Max Langhof Apr 30 '21 at 14:35
  • @MaxLanghof Consider a situation where the behavior depends on the type of `x` while not explicitly assuming the type. For example the output for `std::cout << sizeof(foo::x)` might be affected, but there is nothing strictly wrong with the code. It is a fair difference between the two implementations of `x`. But I agree with you that it should still be OK, since the original code is aware that the type of `X` is not specified. – François Andrieux Apr 30 '21 at 14:43
  • It should be clarified that in the enum situation, x DOES have an underlying type; it is just not fixed. If this is a library; two different developers might see different sizes from sizeof(foo::x). But the type of X is specified by the implementation. – thelizardking34 Apr 30 '21 at 15:16
  • The answer highlights the main pitfall, but it should be underlined that "not fixed" here implies that the type of the enumerator in the unmodernized version may actually depend on the value of `X`, and talking about "the type of `Foo::x`" is nonsensical. The type of `Foo<1>::x` may be `char` while the type of `Foo<65537>::x` may be `int`. – Laurent LA RIZZA May 06 '21 at 08:40
2

It should be safe, you just have limited what language standards allow your code to compile. If you don't want to restrict the code to only working in C++17, then you can use

template <int X>
struct Foo
{
  static constexpr int x;
};

template <int X> 
constexpr int Foo<X>::x = X;

This will allow the code to work with C++11. C++17 has an explicit exemption for this construct as it would break a lot of code if all of the sudden the out of line definitions cause an ODR violation.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
NathanOliver
  • 171,901
  • 28
  • 288
  • 402