From eac6afacdc12a9c6f1f0ef031059f5156f669a90 Mon Sep 17 00:00:00 2001 From: Anton Bachin Date: Sat, 11 Jul 2015 19:32:28 -0500 Subject: [PATCH] Bidirectional maps between enums and any types. Also added a C++14 configuration to testing, but using it only for bidrectional map testing at the moment. --- doc/tutorial/5-map.md | 77 +++++++++++++++++++ .../{5-iostreams.md => 6-iostreams.md} | 0 doc/tutorial/{6-safety.md => 7-safety.md} | 0 ...-representation.md => 8-representation.md} | 0 .../{8-constexpr.md => 9-constexpr.md} | 0 enum.h | 63 +++++++++++++++ example/5-map.cc | 74 ++++++++++++++++++ example/{5-iostreams.cc => 6-iostreams.cc} | 0 example/{6-safety.cc => 7-safety.cc} | 0 ...-representation.cc => 8-representation.cc} | 0 example/{8-constexpr.cc => 9-constexpr.cc} | 0 test/CMakeLists.txt | 34 +++++++- test/Makefile | 3 + test/expect/5-map | 4 + test/expect/{5-iostreams => 6-iostreams} | 0 test/expect/{6-safety => 7-safety} | 0 .../{7-representation => 8-representation} | 0 test/expect/{8-constexpr => 9-constexpr} | 0 18 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 doc/tutorial/5-map.md rename doc/tutorial/{5-iostreams.md => 6-iostreams.md} (100%) rename doc/tutorial/{6-safety.md => 7-safety.md} (100%) rename doc/tutorial/{7-representation.md => 8-representation.md} (100%) rename doc/tutorial/{8-constexpr.md => 9-constexpr.md} (100%) create mode 100644 example/5-map.cc rename example/{5-iostreams.cc => 6-iostreams.cc} (100%) rename example/{6-safety.cc => 7-safety.cc} (100%) rename example/{7-representation.cc => 8-representation.cc} (100%) rename example/{8-constexpr.cc => 9-constexpr.cc} (100%) create mode 100644 test/expect/5-map rename test/expect/{5-iostreams => 6-iostreams} (100%) rename test/expect/{6-safety => 7-safety} (100%) rename test/expect/{7-representation => 8-representation} (100%) rename test/expect/{8-constexpr => 9-constexpr} (100%) diff --git a/doc/tutorial/5-map.md b/doc/tutorial/5-map.md new file mode 100644 index 0000000..3174ee1 --- /dev/null +++ b/doc/tutorial/5-map.md @@ -0,0 +1,77 @@ +## Maps + +It is possible to create `constexpr` bidirectional maps between Better Enums and +any type. This is currently an experimental feature. Feedback is very much +wanted, but please don't build any mission-critical code on top of this :) + +The way it works is you give Better Enums a function — say, +`const char* describe(Channel)`. The library enumerates it to make a map. + +The reason for using a function is that a `switch` statement is, I believe, the +only place where a compiler will check for exhaustiveness. If you forget to +create a case for one of the enum's constants, the compiler can let you know. +Obviously, a `switch` statement is not data, and needs to be inside a function. +It can only be inside a `constexpr` function in $cxx14, so this feature is most +natural in $cxx14. When you pass the function to Better Enums, the library can +build up a lookup data structure at compile time. + +Actually, right now, Better Enums doesn't quite do that — it enumerates +the function *every* time you want to convert to an enum (but not *from* an +enum). It simply does a linear scan every time. This is because I haven't yet +found a data structure whose compile-time generation is fast enough for +practical use. + +--- + + #include + #include <enum.h> + + ENUM(Channel, int, Red, Green, Blue) + +We will create a map from this function: + + constexpr const char* describe(Channel channel) + { + switch(channel) { + case Channel::Red: return "the red channel"; + case Channel::Green: return "the green channel"; + case Channel::Blue: return "the blue channel"; + } + + return "needed for gcc 5.1"; + } + +Here is the map. The actual type is `better_enums::map`. + + constexpr auto descriptions = better_enums::make_map(describe); + +And the usage: + + int main() + { + std::cout << descriptions[Channel::Red] << std::endl; + + std::cout << descriptions.from_enum(Channel::Red) << std::endl; + std::cout << descriptions.to_enum("the green channel") << std::endl; + + auto not_a_literal = std::string("the blue channel"); + std::cout << descriptions.to_enum(not_a_literal.c_str()) << std::endl; + + return 0; + } + +--- + +`make_map` above produces a value of type `better_enums::map`. The full +signature of the template `better_enums::map` is + +~~~comment +template > +~~~ + +`Compare` has to be a class with a static member function +`bool less(const T&, const T&)`. The default implementation +`better_enums::map_compare` simply applies `operator <`, except when `T` is +`const char*`. In that case, it does lexicographic comparison. + +%% description = Mapping enums to arbitrary types and vice versa. diff --git a/doc/tutorial/5-iostreams.md b/doc/tutorial/6-iostreams.md similarity index 100% rename from doc/tutorial/5-iostreams.md rename to doc/tutorial/6-iostreams.md diff --git a/doc/tutorial/6-safety.md b/doc/tutorial/7-safety.md similarity index 100% rename from doc/tutorial/6-safety.md rename to doc/tutorial/7-safety.md diff --git a/doc/tutorial/7-representation.md b/doc/tutorial/8-representation.md similarity index 100% rename from doc/tutorial/7-representation.md rename to doc/tutorial/8-representation.md diff --git a/doc/tutorial/8-constexpr.md b/doc/tutorial/9-constexpr.md similarity index 100% rename from doc/tutorial/8-constexpr.md rename to doc/tutorial/9-constexpr.md diff --git a/enum.h b/enum.h index 668e59b..11500d9 100644 --- a/enum.h +++ b/enum.h @@ -1190,6 +1190,69 @@ BETTER_ENUMS__CONSTEXPR inline bool operator >=(const Enum &a, const Enum &b) \ namespace better_enums { +// Maps. + +template +struct map_compare { + BETTER_ENUMS__CONSTEXPR static bool less(const T& a, const T& b) + { return a < b; } +}; + +template <> +struct map_compare { + BETTER_ENUMS__CONSTEXPR static bool less(const char *a, const char *b) + { return less_loop(a, b); } + + private: + BETTER_ENUMS__CONSTEXPR static bool + less_loop(const char *a, const char *b, size_t index = 0) + { + return + a[index] != b[index] ? a[index] < b[index] : + a[index] == '\0' ? false : + less_loop(a, b, index + 1); + } +}; + +template > +struct map { + typedef T (*function)(Enum); + + BETTER_ENUMS__CONSTEXPR explicit map(function f) : _f(f) { } + + BETTER_ENUMS__CONSTEXPR T from_enum(Enum value) const { return _f(value); } + BETTER_ENUMS__CONSTEXPR T operator [](Enum value) const + { return _f(value); } + + BETTER_ENUMS__CONSTEXPR Enum to_enum(T value) const + { + return + _or_throw(to_enum_nothrow(value), "map::to_enum: invalid argument"); + } + + BETTER_ENUMS__CONSTEXPR optional + to_enum_nothrow(T value, size_t index = 0) const + { + return + index >= Enum::_size() ? optional() : + Compare::less(_f(Enum::_values()[index]), value) || + Compare::less(value, _f(Enum::_values()[index])) ? + to_enum_nothrow(value, index + 1) : + Enum::_values()[index]; + } + + private: + const function _f; +}; + +template +BETTER_ENUMS__CONSTEXPR map make_map(T (*f)(Enum)) +{ + return map(f); +} + + + // Stream I/O operators. // This template is used as a sort of enable_if for SFINAE. It should be diff --git a/example/5-map.cc b/example/5-map.cc new file mode 100644 index 0000000..3ffcef5 --- /dev/null +++ b/example/5-map.cc @@ -0,0 +1,74 @@ +// This file was generated automatically. + +// Maps +// +// It is possible to create constexpr bidirectional maps between Better Enums +// and any type. This is currently an experimental feature. Feedback is very +// much wanted, but please don't build any mission-critical code on top of this +// :) +// +// The way it works is you give Better Enums a function - say, const char* +// describe(Channel). The library enumerates it to make a map. +// +// The reason for using a function is that a switch statement is, I believe, the +// only place where a compiler will check for exhaustiveness. If you forget to +// create a case for one of the enum's constants, the compiler can let you know. +// Obviously, a switch statement is not data, and needs to be inside a function. +// It can only be inside a constexpr function in C++14, so this feature is most +// natural in C++14. When you pass the function to Better Enums, the library can +// build up a lookup data structure at compile time. +// +// Actually, right now, Better Enums doesn't quite do that - it enumerates the +// function every time you want to convert to an enum (but not from an enum). It +// simply does a linear scan every time. This is because I haven't yet found a +// data structure whose compile-time generation is fast enough for practical +// use. + + +#include +#include + +ENUM(Channel, int, Red, Green, Blue) + +// We will create a map from this function: + +constexpr const char* describe(Channel channel) +{ + switch(channel) { + case Channel::Red: return "the red channel"; + case Channel::Green: return "the green channel"; + case Channel::Blue: return "the blue channel"; + } + + return "for gcc 5.1"; +} + +// Here is the map. The actual type is better_enums::map. + +constexpr auto descriptions = better_enums::make_map(describe); + +// And the usage: + +int main() +{ + std::cout << descriptions[Channel::Red] << std::endl; + + std::cout << descriptions.from_enum(Channel::Red) << std::endl; + std::cout << descriptions.to_enum("the green channel") << std::endl; + + auto not_a_literal = std::string("the blue channel"); + std::cout << descriptions.to_enum(not_a_literal.c_str()) << std::endl; + + return 0; +} + + +// make_map above produces a value of type better_enums::map. The full +// signature of the template better_enums::map is +// +// template > +// +// Compare has to be a class with a static member function bool less(const T&, +// const T&). The default implementation better_enums::map_compare simply +// applies operator <, except when T is const char*. In that case, it does +// lexicographic comparison. diff --git a/example/5-iostreams.cc b/example/6-iostreams.cc similarity index 100% rename from example/5-iostreams.cc rename to example/6-iostreams.cc diff --git a/example/6-safety.cc b/example/7-safety.cc similarity index 100% rename from example/6-safety.cc rename to example/7-safety.cc diff --git a/example/7-representation.cc b/example/8-representation.cc similarity index 100% rename from example/7-representation.cc rename to example/8-representation.cc diff --git a/example/8-constexpr.cc b/example/9-constexpr.cc similarity index 100% rename from example/8-constexpr.cc rename to example/9-constexpr.cc diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b8e4e14..2565222 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,6 +14,14 @@ else() set(SUPPORTS_CONSTEXPR 1) endif() +list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_relaxed_constexpr + RELAXED_CONSTEXPR_INDEX) +if(RELAXED_CONSTEXPR_INDEX EQUAL -1) + set(SUPPORTS_RELAXED_CONSTEXPR 0) +else() + set(SUPPORTS_RELAXED_CONSTEXPR 1) +endif() + list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_strong_enums ENUM_CLASS_INDEX) if(ENUM_CLASS_INDEX EQUAL -1) set(SUPPORTS_ENUM_CLASS 0) @@ -28,6 +36,14 @@ if(CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.7) set(SUPPORTS_ENUM_CLASS 0) endif() +# Not supporting C++14 testing on clang++34 due to buggy library installed in +# Travis Ubuntu image. + +if(CMAKE_CXX_COMPILER_ID STREQUAL Clang + AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 3.5) + set(SUPPORTS_RELAXED_CONSTEXPR 0) +endif() + # Select standard based on the requested configuration. If the compiler does not # support the requested configuration, write a message and generate a no-op @@ -66,6 +82,14 @@ elseif(CONFIGURATION STREQUAL STRICT_CONVERSION) endif() elseif(CONFIGURATION STREQUAL CXX98) set(CMAKE_CXX_STANDARD 98) +elseif(CONFIGURATION STREQUAL CXX14) + if(SUPPORTS_RELAXED_CONSTEXPR) + set(CMAKE_CXX_STANDARD 14) + else() + message(WARNING "This compiler does not support relaxed constexpr") + file(WRITE "${DO_NOT_TEST_FILE}") + return() + endif() else() set(CMAKE_CXX_STANDARD 11) endif() @@ -87,12 +111,12 @@ endforeach(TEST) # Select examples to build. set(EXAMPLES - 1-hello-world 2-conversions 3-iterate 4-switch 5-iostreams 6-safety - 7-representation 8-constexpr 101-special-values 102-any-underlying + 1-hello-world 2-conversions 3-iterate 4-switch 6-iostreams 7-safety + 8-representation 9-constexpr 101-special-values 102-any-underlying 103-bitset 104-quine 105-c++17-reflection) set(SKIPPED_FOR_CXX98 - 8-constexpr 101-special-values 102-any-underlying 103-bitset 104-quine + 5-map 9-constexpr 101-special-values 102-any-underlying 103-bitset 104-quine 105-c++17-reflection) set(SKIPPED_FOR_STRICT_CONVERSION 4-switch 102-any-underlying) @@ -105,6 +129,10 @@ if(CONFIGURATION STREQUAL STRICT_CONVERSION) list(REMOVE_ITEM EXAMPLES ${SKIPPED_FOR_STRICT_CONVERSION}) endif() +if(CONFIGURATION STREQUAL CXX14) + set(EXAMPLES 5-map) +endif() + foreach(EXAMPLE ${EXAMPLES}) add_executable(example-${EXAMPLE} ../example/${EXAMPLE}.cc) endforeach(EXAMPLE) diff --git a/test/Makefile b/test/Makefile index 278a367..ab056b7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -125,6 +125,9 @@ all-configurations : make TITLE=$(TITLE)-enum-class \ CMAKE_OPTIONS="$(CMAKE_OPTIONS) -DCONFIGURATION=STRICT_CONVERSION" \ one-configuration + make TITLE=$(TITLE)-c++14 \ + CMAKE_OPTIONS="$(CMAKE_OPTIONS) -DCONFIGURATION=CXX14" \ + one-configuration make TITLE=$(TITLE)-c++98 \ CMAKE_OPTIONS="$(CMAKE_OPTIONS) -DCONFIGURATION=CXX98" \ one-configuration diff --git a/test/expect/5-map b/test/expect/5-map new file mode 100644 index 0000000..6df9530 --- /dev/null +++ b/test/expect/5-map @@ -0,0 +1,4 @@ +the red channel +the red channel +Green +Blue diff --git a/test/expect/5-iostreams b/test/expect/6-iostreams similarity index 100% rename from test/expect/5-iostreams rename to test/expect/6-iostreams diff --git a/test/expect/6-safety b/test/expect/7-safety similarity index 100% rename from test/expect/6-safety rename to test/expect/7-safety diff --git a/test/expect/7-representation b/test/expect/8-representation similarity index 100% rename from test/expect/7-representation rename to test/expect/8-representation diff --git a/test/expect/8-constexpr b/test/expect/9-constexpr similarity index 100% rename from test/expect/8-constexpr rename to test/expect/9-constexpr