Type Safe C++ enum Extensions

Type Safe C++ enum Extensions

By Alf Steinbach

Overload, 31(175):15-16, June 2023


Is it possible to extend a value type in C++? Alf Steinbach describes how to extend enum values.

Consider if an enum like the following,

  enum class Suit{
  spades, hearts, diamonds, clubs };

could be extended like

  enum class Suit_with_joker extends Suit {
    joker };

where

  • Suit_with_joker has all the enumerators of Suit plus the joker enumerator; and
  • enumerators introduced in Suit_with_joker get integer values following those of Suit; and
  • any Suit value is also a Suit_with_joker value.

This would be an example of what I’ll call a value type extension.

The apparently backwards is-a relationship in the last point, where any value of the original type is-a value of the derived type, is characteristic of value type extensions.

C++20 totally lacks support for value type extensions, of enum types or other types.

Value type ‘is-a’ versus class inheritance ‘is-a’

Direct use of class inheritance to model an enum extension would give an is-a relationship the wrong way.

As a concrete example, see Listing 1.

struct Suit
{
  int value;

  constexpr explicit Suit( const int v )
    : value( v ) {}

    static const Suit spades;
    static const Suit hearts;
};

inline constexpr Suit Suit::spades = Suit( 0 );
inline constexpr Suit Suit::hearts = Suit( 1 );

struct Suit_with_joker: Suit
{
  constexpr explicit
    Suit_with_joker( const int v ): Suit( v ) {}
  static const Suit_with_joker joker;
};

inline constexpr Suit_with_joker
  Suit_with_joker::joker = Suit_with_joker( 4 );

auto main() -> int
{
  (void) Suit_with_joker::hearts;
      // OK, has inherited the "enumerators".
  Suit_with_joker s1 = Suit::hearts;
      //! C. error, wrong way is-a relationship.
  Suit s2 = Suit_with_joker::joker;
      //! No c. error, but should be error.
}
Listing 1

So, class inheritance works for picking up the base type enumerators, but it doesn’t work for expressing the backwards is-a relationship between base value type and extended type.

A type safe model of an enum extension

Instead of providing reference conversion via class inheritance, a model of an enum extension requires value conversion via constructors and/or type conversion operators.

This is how e.g. unique_ptr works. A unique_ptr<Derived>& reference is not a unique_ptr<Base>& reference – there’s no inheritance relationship! But a unique_ptr<Derived> value converts to a unique_ptr<Base> value.

When Suit_with_joker doesn’t inherit Suit (since that would be the wrong way) it must inherit in the Suit enumerators from somewhere else. Which means that the enumerators must be defined in parallel enumerator holder classes. With no support for comparisons, data hiding etc., just implementing type safe conversion, it can go like Listing 2.

struct Suit;
struct Suit_names
{
  static const Suit spades;
  static const Suit hearts;
};
struct Suit:
  Suit_names
{
  int value;
  constexpr explicit Suit( const int v )
    : value( v ) {}
};
constexpr Suit Suit_names::spades = Suit( 0 );
constexpr Suit Suit_names::hearts = Suit( 1 );

struct Suit_with_joker;
struct Suit_with_joker_names:
  Suit_names
{
  static const Suit_with_joker joker;
};
struct Suit_with_joker:
  Suit_with_joker_names
{
  int value;
  constexpr explicit
    Suit_with_joker( const int v ): value( v ) {}
  constexpr Suit_with_joker( const Suit v )
    : value( v.value ) {}
};
constexpr Suit_with_joker
  Suit_with_joker_names::joker
  = Suit_with_joker( 4 );
auto main() -> int
{
  (void) Suit_with_joker::hearts;
    // OK, has inherited the "enumerators".
  Suit_with_joker s1 = Suit::hearts;
    // OK, right way is-a relationship.
  #ifdef FAIL_PLEASE
    Suit s2 = Suit_with_joker::joker;
      //! C. error, /as it should be/. :)
  #endif
}
Listing 2

Compared to the hypothetical

  enum class Suit{
    spades, hearts, diamonds, clubs };
  enum class Suit_with_joker extends Suit {
    joker };

… this is a heck of a lot of code; language support would have been nice.

Note: the above code just exemplifies working C++ that implements a type safe enumeration type extension. It does not provide conversion from enumerator to int, or more generally to the underlying type. And it does not provide a way to specify the underlying type. As mentioned, it does not provide value comparison.

Suit_names and Suit_with_joker_names should be non-instantiable. And Suit and Suit_with_joker should ideally inherit in the value data member from some generic Enumeration class. And there are even more issues, all omitted for clarity, but all mostly trivial.

About an enum extension syntax

In the example I used the word extends instead of just a colon : as with classes, because this isn’t like a class inheritance: the is-a relationship goes the opposite way.

And I imagine that a useful syntax would have to provide for a list of base enum types, not just one.

Case in point: in my own hobbyist code I’ve only used the above scheme once, mostly as an exploration of the issues, and then for data stream id’s hypothetically defined like

  enum class Input_stream_id{ in = 0 };
  enum class Output_stream_id{ out = 1, err = 2 };
  enum class Stream_id extends Input_stream_id,
    Output_stream_id {};

This supported type safety for functions taking a stream id, since a stream id argument can be limited to input (Input_stream_id parameter type) or output (Output_stream_id parameter type), and alternatively can be allowed to be any stream (Stream_id parameter type).

Probably the Boost Preprocessing Library (BPL) can be used to generate modeling code for enumeration type extensions. I.e. the hypothetical enum declarations can be replaced with actual C++ macro invocations. And possibly, as a less maintenance-friendly alternative, AI based code generation such as via ChatGPT can be used on a case by case base.

However, regarding BPL-based macros, in my experience ‘smart’ variadic macros lead to brittle and ungrokable code. If or when one chooses to use type safe enumeration extensions, it is perhaps better to just code it up manually, as I did for the stream id’s. I believe that this is an example of a feature that would be used if it was provided by the core language, where the centralized effort provides correctness guarantees and the effort involved confers some advantage to all millions of C++ users.

Alf Steinbach is a Norwegian C++ enthusiast, currently co-admin of FB groups ‘C++ Enthusiasts’ and ‘C++ in-practice questions (most anything!)’. He’s worked as a vocational school teacher (no C++), as a college lecturer (teaching also C++, and introducing a Windows programming course), and as an IT consultant (mostly C and C++).







Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.