Because I think maintainability can be done better with strings instead of plain enumerator-values when doing fstream IO, I wanted a replacement struct in my project that behaves like an enum class but provides string functionality. It turned out to be a project on its own. There are other implementations to achieve this kind of functionality, but for me it's also a learning process. Three generic questions:
- What did I miss in trying to mimic
enum classbehavior (see main())? - What rules did I break, things I forgot, when putting this code together (I do not see myself as a very good programmer, happy when things just work...)?
- I read somewhere to use
noexceptwherever possible. Is that a correct practice?
Other question: I have the following:
// Conversion operator, used for static_cast.
explicit operator T() const noexcept {
return value;
}
Would it be better to have this templated itself like:
// Conversion operator, used for static_cast.
template<class U>
explicit operator U() const noexcept {
return static_cast<U>(value);
}
If this is not the right place or format, please let me know.
Required: C++20, and /Zc:preprocessor for MSVC++ (which I used). Code is in one file now for simplicity.
#include <cstring>
#include <iostream>
#include <string>
//---------------------------------------------------------------------------
// The folowing set of macros was written by David Mazières:
//
// https://www.scs.stanford.edu/~dm/blog/va-opt.html
//
// I added prefix 'EB_' to prevent name-collisions with other macros.
//
// Note: In MSVC++ you need the switch '/Zc:preprocessor'.
//---------------------------------------------------------------------------
// These expand macros enable over 300 arguments for FOR_EACH.
#define EB_EXPAND(...) EB_EXPAND4(EB_EXPAND4(EB_EXPAND4(EB_EXPAND4(__VA_ARGS__))))
#define EB_EXPAND4(...) EB_EXPAND3(EB_EXPAND3(EB_EXPAND3(EB_EXPAND3(__VA_ARGS__))))
#define EB_EXPAND3(...) EB_EXPAND2(EB_EXPAND2(EB_EXPAND2(EB_EXPAND2(__VA_ARGS__))))
#define EB_EXPAND2(...) EB_EXPAND1(EB_EXPAND1(EB_EXPAND1(EB_EXPAND1(__VA_ARGS__))))
#define EB_EXPAND1(...) __VA_ARGS__
// A set of parens.
#define EB_PARENS ()
// The actual FOR_EACH macro.
#define EB_FOR_EACH(macro, ...) \
__VA_OPT__(EB_EXPAND(EB_FOR_EACH_HELPER(macro, __VA_ARGS__)))
#define EB_FOR_EACH_HELPER(macro, a1, ...) \
macro(a1) \
__VA_OPT__(EB_FOR_EACH_AGAIN EB_PARENS (macro, __VA_ARGS__))
#define EB_FOR_EACH_AGAIN() EB_FOR_EACH_HELPER
//---------------------------------------------------------------------------
// My own code starts here.
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Macro: count ___VA_ARGS__.
//---------------------------------------------------------------------------
#define EB_COUNT_VA_ARGS_PLUS_ONE(dummy) +1
#define EB_COUNT_VA_ARGS(...) 0 EB_FOR_EACH(EB_COUNT_VA_ARGS_PLUS_ONE, __VA_ARGS__)
//---------------------------------------------------------------------------
// FOR_EACH variant: fixed argument 1 (FA1 = Fix Arg 1) for the macro. Used
// for generating names-pairs like
//
// "Smaller", "Enum::Smaller",
// "Greater", "Enum::Greater",
//
// where "Enum" is the fixed argument for the macro that generates each line.
//---------------------------------------------------------------------------
// The FOR_EACH macro with a fixed first argument (FA1 = Fix Arg 1) for the processing macro.
#define EB_FOR_EACH_FA1(macro, fix1, ...) \
__VA_OPT__(EB_EXPAND(EB_FOR_EACH_HELPER_FA1(macro, fix1, __VA_ARGS__)))
#define EB_FOR_EACH_HELPER_FA1(macro, fix1, arg, ...) \
macro(fix1, arg) \
__VA_OPT__(EB_FOR_EACH_AGAIN_FA1 EB_PARENS (macro, fix1, __VA_ARGS__))
#define EB_FOR_EACH_AGAIN_FA1() EB_FOR_EACH_HELPER_FA1
//---------------------------------------------------------------------------
// FOR_EACH variant: fixed arguments 1 and 2, plus a counter (FA12_CNT = Fix
// Arg 1/2, plus a CNT). Called by macro 'EB_GENERATE_ENUMERATORS'.
//---------------------------------------------------------------------------
#define EB_FOR_EACH_FA12_CNT_PLUS_ONE(cnt) cnt+1
#define EB_FOR_EACH_FA12_CNT(macro, fix1, fix2, ...) EB_FOR_EACH_FA12_CNT_WORKER(macro, fix1, fix2, 0, __VA_ARGS__)
#define EB_FOR_EACH_FA12_CNT_WORKER(macro, fix1, fix2, cnt, ...) \
__VA_OPT__(EB_EXPAND(EB_FOR_EACH_HELPER_FA12_CNT(macro, fix1, fix2, cnt, __VA_ARGS__)))
#define EB_FOR_EACH_HELPER_FA12_CNT(macro, fix1, fix2, cnt, arg, ...) \
macro(fix1, fix2, cnt, arg) \
__VA_OPT__(EB_FOR_EACH_AGAIN_FA12_CNT EB_PARENS (macro, fix1, fix2, EB_FOR_EACH_FA12_CNT_PLUS_ONE(cnt), __VA_ARGS__))
#define EB_FOR_EACH_AGAIN_FA12_CNT() EB_FOR_EACH_HELPER_FA12_CNT
//---------------------------------------------------------------------------
// Macros to build the enum-struct.
//---------------------------------------------------------------------------
// Macro that generates an enumerator as a struct-member with the correct value.
#define EB_GENERATE_ENUMERATOR(eb_type, eb_enum, eb_value, eb_name) static inline constexpr EnumBuilderEnumerator<eb_type, eb_enum> eb_name = GetWrappedValue(static_cast<eb_type>(eb_value));
#define EB_GENERATE_ENUMERATORS(eb_type, eb_enum, ...) EB_FOR_EACH_FA12_CNT(EB_GENERATE_ENUMERATOR, eb_type, eb_enum, __VA_ARGS__)
// Macro that generates a pair of names.
#define EB_GENERATE_NAMES_PAIR(eb_enum, eb_name) #eb_name, #eb_enum"::"#eb_name,
// This is the macro that builds that 'enum class'.
#define ENUM_CLASS_BUILDER(eb_type, eb_enum, ...) \
struct eb_enum final : public EnumBuilderBase<eb_type, eb_enum> { \
eb_enum() : EnumBuilderBase(EnumBuilderEnumerator<eb_type, eb_enum>::invalid_value) {} \
eb_enum(const EnumBuilderEnumerator<eb_type, eb_enum>& e) : EnumBuilderBase(e) {} \
using EnumBuilderBase<eb_type, eb_enum>::operator =; \
EB_GENERATE_ENUMERATORS(eb_type, eb_enum, __VA_ARGS__) \
constexpr size_t Count() const noexcept \
{ \
return static_cast<size_t>(EB_COUNT_VA_ARGS(__VA_ARGS__)); \
} \
const char* ToString(const ToStringMode mode) const noexcept override \
{ \
return ToStringWorker(mode, names, sizeof(names) / sizeof(const char*)); \
} \
const EnumBuilderEnumerator<eb_type, eb_enum> Enumerator(const char* name) const noexcept override \
{ \
return EnumeratorWorker(name, #eb_enum"::", names, sizeof(names) / sizeof(const char*)); \
} \
private: \
static inline constexpr const char* names[] = { \
EB_FOR_EACH_FA1(EB_GENERATE_NAMES_PAIR, eb_enum, __VA_ARGS__) \
"ClosingDummy" \
}; \
};
//---------------------------------------------------------------------------
// Struct that wraps an enumerator. It gives us tools to control type-safety.
//---------------------------------------------------------------------------
// Forward declaration, needed for friend declaration inside 'EnumBuilderEnumerator'.
template<class T = int, class E = void>
struct EnumBuilderBase;
// Wrapper for the enumerator.
template<class T, class E> requires std::integral<T>
struct EnumBuilderEnumerator {
static inline constexpr T invalid_value = static_cast<T>(-1);
// Class 'EnumBuilderBase' is a friend.
template<typename Tf, typename Ef>
friend struct EnumBuilderBase;
// Get the value.
T Value() const noexcept
{
return value;
}
// Checks if the enumerator has a valid value.
bool IsValid() const noexcept
{
return value != invalid_value;
}
// Conversion operator, used for static_cast.
explicit operator T() const noexcept {
return value;
}
// Comparison.
auto operator <=> (const EnumBuilderEnumerator<T, E>& other) const noexcept
{
return value <=> other.value;
}
bool operator == (const EnumBuilderEnumerator<T, E>& other) const noexcept
{
return value == other.value;
}
// Stream operator (only output-stream, as 'EnumBuilderEnumerator' is a constexpr in our enums).
friend static std::ostream& operator << (std::ostream& os, const EnumBuilderEnumerator<T, E>& v) noexcept
{
return os << v.value;
}
private:
// Private, so only friend 'EnumBuilderBase' can use them.
explicit constexpr EnumBuilderEnumerator(const T value) noexcept : value(value) {}
T value;
};
//---------------------------------------------------------------------------
// Code for the enum-struct baseclass. This provides:
//
// - the actual value for an enum-variable
// - constructors
// - assignment operators
// - stream operators
// - conversions between string/value
// - plus some other things
//---------------------------------------------------------------------------
template<class T, class E>
struct EnumBuilderBase {
// Stream-mode.
enum class StreamMode {
Value, // Read/write the value.
EnumeratorOnly, // Write string like 'Enumerator', no effect on read.
IncludeEnumName // Write string like 'MyEnum::Enumerator', no effect on read.
};
// ToString-mode.
enum class ToStringMode {
EnumeratorOnly, // Return string like 'Enumerator'.
IncludeEnumName // Return string like 'MyEnum::Enumerator'.
};
// Constructors.
EnumBuilderBase(const T v) noexcept : value(v), mode(StreamMode::Value) {}
EnumBuilderBase(const EnumBuilderEnumerator<T, E>& e) noexcept : value(e.value), mode(StreamMode::Value) {}
// Must be overridden by the derived class.
virtual const EnumBuilderEnumerator<T, E> Enumerator(const char* name) const noexcept = 0;
virtual const char* ToString(const ToStringMode mode) const noexcept = 0;
// Conversion operator, used for static_cast.
explicit operator T() const noexcept {
return value;
}
// Consider all assignment operators as 'not allowed'.
template<class U>
void operator = (const U) = delete;
// Assignment operators that are allowed.
void operator = (const EnumBuilderBase<T, E>& v) noexcept
{
value = v.value;
}
void operator = (const EnumBuilderEnumerator<T, E>& v) noexcept
{
value = v.value;
}
void operator = (const char* s) noexcept
{
value = Enumerator(s).value;
}
// Get the value.
T Value() const noexcept
{
return value;
}
// Comparison: enum struct and enum struct.
auto operator <=> (const EnumBuilderBase<T, E>& other) const noexcept
{
return value <=> other.value;
}
bool operator == (const EnumBuilderBase<T, E>& other) const noexcept
{
return value == other.value;
}
// Comparison: enum struct and value wrapper.
friend static auto operator <=> (const EnumBuilderBase<T, E>& lhs, const EnumBuilderEnumerator<T, E>& rhs) noexcept
{
return lhs.value <=> rhs.Value();
}
friend static bool operator == (const EnumBuilderBase<T, E>& lhs, const EnumBuilderEnumerator<T, E>& rhs) noexcept
{
return lhs.value == rhs.Value();
}
// Comparison: value wrapper and enum struct.
friend static auto operator <=> (const EnumBuilderEnumerator<T, E>& lhs, const EnumBuilderBase<T, E>& rhs) noexcept
{
return lhs.Value() <=> rhs.value;
}
friend static bool operator == (const EnumBuilderEnumerator<T, E>& lhs, const EnumBuilderBase<T, E>& rhs) noexcept
{
return lhs.Value() == rhs.value;
}
// Stream-mode setter.
void SetStreamMode(const StreamMode m) noexcept
{
mode = m;
}
// Checks if the enum has a valid value.
bool IsValid() const noexcept
{
return value != EnumBuilderEnumerator<T, E>::invalid_value;
}
// Stream operators.
friend static std::ostream& operator << (std::ostream& os, const EnumBuilderBase<T, E>& v) noexcept
{
if (v.mode == StreamMode::Value)
return os << v.value;
else if(v.mode == StreamMode::EnumeratorOnly)
return os << v.ToString(ToStringMode::EnumeratorOnly);
return os << v.ToString(ToStringMode::IncludeEnumName);
}
friend static std::istream& operator >> (std::istream& is, EnumBuilderBase<T, E>& v) noexcept
{
// Set to invalid first.
v.value = EnumBuilderEnumerator<T, E>::invalid_value;
// Read value.
if (v.mode == StreamMode::Value)
return is >> v.value;
// Read string.
std::string s;
is >> s;
v.value = v.Value(s);
return is;
}
protected:
// Worker method for the ToString() method, called by the derived class.
const char* ToStringWorker(const ToStringMode mode,
const char* const names_array[],
const int names_count) const noexcept
{
size_t index = 2 * static_cast<size_t>(value) + (mode == ToStringMode::EnumeratorOnly ? 0 : 1);
if (index >= names_count)
return "invalid";
return names_array[index];
}
// Worker method for the Enumerator() method, called by the derived class.
static const EnumBuilderEnumerator<T, E> EnumeratorWorker(const char* name,
const char* enum_name,
const char* const names_array[],
const int names_count) noexcept
{
// Check for a valid pointer.
if (name == nullptr)
return EnumBuilderEnumerator<T, E>(EnumBuilderEnumerator<T, E>::invalid_value);
// Skip the enum name in the string ('enum_name::value_name' becomes 'value_name').
const char* name_org = name;
while (*name == *enum_name) {
++name;
++enum_name;
}
if (*enum_name != '\0')
name = name_org;
// Check all names.
for (int i = 0; i < names_count; i += 2) {
if (!std::strcmp(name, names_array[i]))
return EnumBuilderEnumerator<T, E>(i/2);
}
// Nothing found...
return EnumBuilderEnumerator<T, E>(EnumBuilderEnumerator<T, E>::invalid_value);
}
// Get an initialized wrapper. The derived class calls this method because it cannot
// directly call the private ctor of 'EnumBuilderEnumerator'.
static constexpr EnumBuilderEnumerator<T, E> GetWrappedValue(const T v) noexcept
{
return EnumBuilderEnumerator<T, E>(v);
}
private:
// Determines if we read/write a value or a string from/to a stream.
StreamMode mode;
// Actual value for enum-variable.
T value;
};
//----------------------------------------------------
// TEST SECTION.
//----------------------------------------------------
// Create our enums to test with.
ENUM_CLASS_BUILDER(int, SmartEnum, Smaller, Greater)
ENUM_CLASS_BUILDER(int, SmartEnumOther, Smaller, Greater)
// Print a single header-line.
void header_line()
{
std::cout << "*---------------------------------------------------------\n";
}
// Print a complete header.
void header(const char* s)
{
header_line();
std::cout << s;
header_line();
}
// Test-code.
int main()
{
SmartEnum smart_s, smart_g;
// Assignment.
smart_s = smart_g;
smart_s = SmartEnum::Smaller;
smart_g = SmartEnum::Greater;
// Assignment with a fundamental type.
// smart_s = 0; // Error, expected.
// Convert to a fundamental type.
// int a = SmartEnum::Smaller; // Error, expected.
// int b = smart_s; // Error, expected.
int c = static_cast<int>(SmartEnum::Smaller);
int d = static_cast<int>(smart_s);
// Number of enumerators.
header("* Number of enumerators, expected output: 2\n");
std::cout << smart_s.Count() << "\n\n";
// Comparison.
header("* Enum comparison, expected output: 0 1 1 0 0 1 0 1\n");
std::cout << (smart_s == smart_g) << ' ';
std::cout << (smart_s == SmartEnum::Smaller) << ' ';
std::cout << (SmartEnum::Smaller == smart_s) << ' ';
std::cout << (SmartEnum::Smaller == SmartEnum::Greater) << ' ';
std::cout << (smart_s > smart_g) << ' ';
std::cout << (smart_g > smart_s) << ' ';
std::cout << (SmartEnum::Smaller > smart_g) << ' ';
std::cout << (SmartEnum::Smaller < SmartEnum::Greater) << ' ';
std::cout << "\n\n";
// Some initializations, and comparison with other enum.
SmartEnumOther smart_s_other = SmartEnumOther::Smaller;
SmartEnumOther smart_s_another(SmartEnumOther::Smaller);
// smart_s == smart_s_other; // Error, expected.
// smart_s == SmartEnumOther::Smaller; // Error, expected.
// SmartEnum::Smaller == SmartEnumOther::Smaller; // Error, expected.
// Streams and strings (non-standard functionality).
header_line();
std::cout << "* Streams and strings, expected output:\n";
std::cout << "* 0\n";
std::cout << "* 1\n";
std::cout << "* 0\n";
std::cout << "* 1\n";
std::cout << "* Smaller\n";
std::cout << "* SmartEnum::Smaller\n";
std::cout << "* Smaller\n";
std::cout << "* SmartEnum::Smaller\n";
header_line();
std::cout << smart_s << '\n';
std::cout << smart_g << '\n';
std::cout << SmartEnum::Smaller << '\n';
std::cout << SmartEnum::Greater << '\n';
std::cout << smart_s.ToString(SmartEnum::ToStringMode::EnumeratorOnly) << '\n';
std::cout << smart_s.ToString(SmartEnum::ToStringMode::IncludeEnumName) << '\n';
smart_s.SetStreamMode(SmartEnum::StreamMode::EnumeratorOnly);
std::cout << smart_s << '\n';
smart_s.SetStreamMode(SmartEnum::StreamMode::IncludeEnumName);
std::cout << smart_s << '\n';
std::cout << "\n";
// Assignment with string.
smart_s.SetStreamMode(SmartEnum::StreamMode::Value);
header("* String assignment, expected output: -1 0 1\n");
smart_s = "some invalid string";
std::cout << smart_s << ' ';
smart_s = "Smaller";
std::cout << smart_s << ' ';
smart_s = "SmartEnum::Greater";
std::cout << smart_s << ' ';
std::cout << "\n\n";
// Valid/invalid.
header("* Valid/invalid, expected output: 0 1\n");
smart_s = "some invalid string";
std::cout << smart_s.IsValid() << ' ';
smart_s = "Smaller";
std::cout << smart_s.IsValid() << ' ';
std::cout << "\n\n";
// Get underlying enumerator for a string.
header("* Enumerator by string, expected output: -1 0 1\n");
std::cout << smart_s.Enumerator("some invalid string") << ' ';
std::cout << smart_s.Enumerator("Smaller") << ' ';
std::cout << smart_s.Enumerator("SmartEnum::Greater") << ' ';
std::cout << "\n\n";
}
to_string()an enum value... \$\endgroup\$std::ranges::find_if()seems wrong on astd::map. It's linear, wherestd::map::find()is logarithmic, in the size of the map. (Or perhaps usestd::map::at()so the caller gets an exception for out-of-range values). \$\endgroup\$enums, in any sense (just trystatic_assert(std::is_enum_v<SmartEnum>)); and 2) even if you want to pretend what you’re making is “enum-ish”, you can’t set values for enumerators (vitally important for flags enumerations), and you can’t alias multiple enumerators to the same value (sometimes important). 4/4 \$\endgroup\$