c++reflectionlanguage-lawyerc++26

Can reflection statements be directly used in if-constexpr?


Below example is taken from P2996 verbatim:

#include <experimental/meta>
#include <string>
#include <type_traits>
#include <print>

template<typename E, bool Enumerable = std::meta::is_enumerable_type(^^E)>
  requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
  if constexpr (Enumerable) {
    template for (constexpr auto e : define_static_array(enumerators_of(^^E)))
      if (value == [:e:])
        return identifier_of(e);
  }
  return "<unnamed>";
}

int main() {
  enum Color : int;
  static_assert(enum_to_string(Color(0)) == "<unnamed>");
  std::println("Color 0: {}", enum_to_string(Color(0)));  // prints '<unnamed>'

  enum Color : int { red, green, blue };
  static_assert(enum_to_string(Color::red) == "red");
  static_assert(enum_to_string(Color(42)) == "<unnamed>");
  std::println("Color 0: {}", enum_to_string(Color(0)));  // prints 'red'
}

I didn't understand why it is necessary to declare Enumerable as a template constant instead of directly calling is_enumerable_type within constexpr.

I tried modifying this part:

template<typename E, bool Enumerable = std::meta::is_enumerable_type(^^E)>
  requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
  if constexpr (Enumerable) {

as this:

template<typename E>
  requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
  if constexpr (std::meta::is_enumerable_type(^^E)) {

but it didn't compile in the experimental P2996 clang implementation:

    Output of x86-64 clang (experimental P2996) (Compiler #1)

<source>:24:17: error: static assertion failed due to requirement 'enum_to_string(Color::red) == "red"'
   24 |   static_assert(enum_to_string(Color::red) == "red");
      |   

https://godbolt.org/z/38enPGjoY

Why doesn't the second example work?

Is this a limitation of the prototype implementation or is it due to some requirements by specs?


Solution

  • Paring the main function to:

      enum Color : int;
      static_assert(enum_to_string(Color(0)) == "<unnamed>");
      enum Color : int { red, green, blue };
      static_assert(enum_to_string(Color::red) == "red");
    

    With the default template argument, these two calls become enum_to_string<Color, false>(Color(0)) and enum_to_string<Color, true>(Color::red).

    Without it, they are both enum_to_string<Color>(Color(0)) (or Color::red which is the same value). This leads to an ODR violation because std::meta::is_enumerable_type(^^E) would return a different value.

    [temp.point]p7:

    If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.

    [basic.def.ord]p(15.9):

    • In each such definition, corresponding manifestly constant-evaluated expressions that are not value-dependent shall have the same value ([expr.const], [temp.dep.constexpr]).

    This is not a problem with the default argument version because enum_to_string<Color, false> and enum_to_string<Color, true> are different functions (and the value is always the same in enum_to_string<Color, false> and enum_to_string<Color, true>)

    It is similar to if you had done this:

    template<typename E, bool B = requires { E::red; }>
    constexpr bool has_red_good(E) { return B; }
    
    template<typename E>
    constexpr bool has_red_bad(E) { return requires { E::red; }; }
    
    int main() {
        enum Color : int;
        static_assert(!has_red_good(Color(0)));
        static_assert(!has_red_bad(Color(0)));
        enum Color : int { red, green, blue };
        static_assert(has_red_good(Color(0)));
        static_assert(!has_red_bad(Color(0)));  // Ill-formed NDR
    }