Empty Scoped Enums as Strong Aliases for Integral Types

Empty Scoped Enums as Strong Aliases for Integral Types

By Lukas Böger

Overload, 27(152):9-10, August 2019


Scoped enums have many advantages. Lukas Böger demonstrates their use as strong types of numbers.

Scoped enumerations were one of the easy-to-grasp C++11 features that quickly spread and became the intended, superior alternative to their unscoped siblings. Local enumerator scope and forward declarations reduce namespace pollution and compilation dependencies, while prohibited implicit conversions to integral types promote type safety.

  enum class Season {winter, spring, summer,
    autumn};

  Season s1 = Season::spring; // Ok


  // Error, no conversion to int:
  int s2 = Season::summer;
  // Error, must be Season::summer:
  Season s3 = summer;

As with unscoped enums, it is possible to create an enumerator object from objects of its underlying type. In C++14, this is the way to go:

  auto s4 = Season(1);
  auto s5 = static_cast<Season>(2);

Both versions are equivalent; an explicit type conversion on the right hand side (functional notation and cast expression) is used for copy initializing s4 and s5. The necessity to detour via copy initialization seems clumsy though, the static_cast version even looks like someone forced the compiler to perform a dubious conversion without warnings. Thanks to P0138 [ Reis16 ], C++17 mitigates this scenario by allowing for direct list initialization of scoped enums (braces mandatory, no narrowing conversions).

  Season s6{1};

But why even try to construct an enumeration from a literal? Does such an initialization not defeat the whole purpose of an enum , i.e., accessing a set of constants via comprehensible names instead of magic numbers? This concern is justified, and auto s = Season::spring should indeed be the preferable way to initialize the above enumerator. But recall that enumerations without any explicit enumerator are valid, too, and then, there is no comprehensible name at hand. Empty scoped enumerations can be extraordinarily useful as strong type aliases for integral types, and the new list initialization adds the missing piece for their mainstream usage as such. But let’s first cover some ground.

What is a strong type alias and what problem does it solve?

Type aliases in C++ (via the typedef or using keyword) introduce new type names, but not new types. They are transparent : various type aliases referring to the same underlying type can be interchanged without errors or even warnings.

  using InventoryId = int;
  using RoomNumber = int;
  
  void store(InventoryId what, RoomNumber where);
  
  // Ok, nothing but ints (bad!):
  store(RoomNumber{2}, InventoryId{10});

Strengthening the restrictions on a type alias with respect to substitutability and computational base (its associated functionality) renders it a strong type alias or a strong typedef . This requires a distinct type and is used to enforce semantics at compile time and to improve the expressiveness of function parameters. Assuming a StrongTypeDef template at hand, the above example could be written as

  // Use a tag type as 2nd template parameter to
  // create unique types:
  using InventoryId = StrongTypeDef<int, 
    struct InventoryIdTag>;
  using RoomNumber = StrongTypeDef<int, 
    struct RoomNumberTag>;
  
  void store(InventoryId what, RoomNumber where);
  
  // Error, types don't match (good!):
  store(RoomNumber{2}, InventoryId{10});

References to such techniques are numerous, see e.g. Matthew Wilson’s early outline and example implementation [ Wilson03 ], Scott Meyer’s ‘Make interfaces easy to use correctly and hard to use incorrectly’ in Effective C++ [ Meyers05 ] or Ben Deane’s talk ‘Using Types Effectively’ [ Deane16 ]. Exemplary implementations for internal purposes can be found in the Boost Serialization library [ Ramey ], Llvm [ Lattner04 ] or Chromium [ Chromium ], while distinct libraries with strong type templates are e.g. type_safe [ Müller ] and Named Type [ Boccara ].

What is the design space of strong type aliases?

The smallest and most restrictive set of operations is explicit construction from the underlying type and explicit conversion to it. The other extreme is a mirror of the complete computational base of the wrapped type (in case of an int , this includes bitshifting, modulo operators and so on). Most approaches are somewhere in the middle. Their design requires answers to the following questions.

  • Type safety and constructability: allow implicit conversions from or to the underlying type (both doesn’t make any sense)? Provide a default constructor?
  • Uniqueness upon reuse: create a new type by an additional tag type template parameter or wrap the strong typedef definition into a macro?
  • Comparison and arithmetic operators: when wrapping types that support those, mirror a subset? When construction is explicit, should binary functions be duplicated for one parameter of the underlying type?
  • Hashing, serialization, parsing: support insertion into std::unordered_set / map ? Offer operator<< and/or operator>> for standard library streams? Support the upcoming fmt library?

Finding agreeable answers for these questions is hard. An attempt to standardize ‘Opaque Typedefs’ as first-class C++ citizens could not succeed, see N3741 [ Brown18 ], and hence, when a strong typedef is needed, we must choose a library solution or ship our own – except when the wrapped type is an integral one.

How do empty scoped enumerators fit in?

Let’s clearly state that once again: scoped enumerators are restricted with respect to the wrapped type: it must be an integral type ( bool , int , unsigned short , etc.). When a double or a std::string are involved, you are out of luck. But it turns out that integral types are the most commonly used ones; a quick-and-dirty regex scan of the Chromium sources showed that around half of all strong types wrap integral values. This is what the above example looks like with an empty enumeration:

  enum class InventoryId {};
  enum class RoomNumber {};
  
  void store(InventoryId what, RoomNumber where);
  
  // Error, types don't match (good!):
  store(RoomNumber{2}, InventoryId{10});

Scoped enumerators must be explicitly constructed. Narrowing conversions during construction are invalid (which cannot even be enforced with a generic library type), default construction is allowed and yields zero or false . Retrieval of the underlying type requires a cast (functional, C-style or static_cast ), so no laziness here. By default, the underlying type of a scoped enumeration is int , but this can be adjusted. Every definition creates a completely new type, but their definition is trivial – this is a clean, built-in solution, requiring neither a macro nor an additional tag type. Objects of one type are totally ordered through the usual comparison operators, and std::hash works out of the box. Standard arithmetic or IO operations are not supported. Manually adding them as needed is straightforward, though admittedly, with many such enumerators, you will either end up polluting some namespace with greedy operator templates or go with a macro to not repeatedly implement the same functions for different types 1 .

So when am I supposed to use empty scoped enumerations?

Every time a strong integral type seems handy for a function signature, an API, a vocabulary type in your project – empty scoped enums should be your first consideration. These types are dead simple, they are as efficient as expressive and require no external dependency. C++17 makes them easy to instantiate, there is no burden left that keeps you from leveraging their strengths. Just keep that in mind for the next time you write an interface that uses integral parameters!

References

[Boccara] Jonathan Boccara et al. , Named Type, a header-only library for strong types. On GitHub at https://github.com/joboccara/NamedType/blob/master/named_type_impl.hpp

[Brown18] Walter Brown (2018) ‘Toward Opaque Typedefs for C++1Y’, v2 ISO/IEC JTC1/SC22/WG21 document N3741, 2018-08-30. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3741.pdf

[Chromium] Chromium Project, sources as retrieved in June 2019 from https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/base/util/type_safety/strong_alias.h

[Deane16] Ben Deane (2016) ‘Using Types Effectively’ from CppCon 2016 , available at https://www.youtube.com/watch?v=ojZbFIQSdl8

[Lattner04] Chris Lattner and Vikram Adve (2004) ‘Llvm: A Compilation Framework for Lifelong Program Analysis and Transformation’ in Proc. of the 2004 International Symposium on Code Generation and Optimization , Palo Alto, California, 2004. The implementation on GitHub is available: https://github.com/llvm/llvm-project/blob/master/llvm/include/llvm/Support/YAMLTraits.h

[Meyers05] Scott Meyers (2005) Effective C++: 55 Specific Ways to Improve Your Programs and Designs , 3rd. edition, Addison-Wesley.

[Müller] Jonathan Müller et al. , type_safe: Zero overhead utilities for preventing bugs at compile time. The implementation on GitHub is available: https://github.com/foonathan/type_safe/blob/master/include/type_safe/strong_typedef.hpp

[Ramey] Robert Ramey et al. , Boost Serialization Library, Version 1.70. BOOST_STRONG_TYPEDEF (documentation) available at https://www.boost.org/doc/libs/1_70_0/libs/serialization/doc/strong_typedef.html

[Reis16] Gabriel Dos Reis, Construction Rules for enum class Values’, ISO/IEC JTC1/SC22/WG21 document P0138, 2016-03-04. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0138r2.pdf

[Wilson03] Matthew Wilson (2003) ‘True typedefs’ in Dr. Dobb’s Journal , dated 01 March 2003. Available at: http://www.drdobbs.com/true-typedefs/184401633

  • Strong type alias templates scale better here, as they provide operators through the Barton-Nackmann trick. This can get out of hand, though; ambitious solutions risk duplicating the Boost Operator Library.

Lukas Böger is a civil engineer who stuck with Fortran77 during his PhD program and started a C++ side project to alleviate his frustration. This worked out, and he now develops power electronics simulation software for Plexim, Zürich. He likes reading, brass music, Newton mechanics and his family..






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.