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.
This commit is contained in:
Anton Bachin 2015-07-11 19:32:28 -05:00
parent 0f816be0cd
commit eac6afacdc
18 changed files with 252 additions and 3 deletions

77
doc/tutorial/5-map.md Normal file
View File

@ -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 <iostream>
<em>#include</em> <<em>enum.h</em>>
<em>ENUM</em>(<em>Channel</em>, <em>int</em>, <em>Red</em>, <em>Green</em>, <em>Blue</em>)
We will create a map from this function:
constexpr <em>const char* describe</em>(<em>Channel channel</em>)
{
switch(<em>channel</em>) {
case <em>Channel::Red</em>: return <em>"the red channel"</em>;
case <em>Channel::Green</em>: return <em>"the green channel"</em>;
case <em>Channel::Blue</em>: return <em>"the blue channel"</em>;
}
return "needed for gcc 5.1";
}
Here is the map. The actual type is `better_enums::map<Channel, const char*>`.
constexpr auto <em>descriptions</em> = <em>better_enums::make_map</em>(<em>describe</em>);
And the usage:
int main()
{
std::cout << <em>descriptions[Channel::Red]</em> << std::endl;
std::cout << <em>descriptions</em>.<em>from_enum</em>(<em>Channel::Red</em>) << std::endl;
std::cout << <em>descriptions</em>.<em>to_enum</em>(<em>"the green channel"</em>) << std::endl;
auto <em>not_a_literal</em> = <em>std::string</em>(<em>"the blue channel"</em>);
std::cout << <em>descriptions</em>.<em>to_enum</em>(<em>not_a_literal</em>.<em>c_str()</em>) << std::endl;
return 0;
}
---
`make_map` above produces a value of type `better_enums::map<E, T>`. The full
signature of the template `better_enums::map` is
~~~comment
<em>template <typename Enum, typename T, typename Compare = map_compare<T>></em>
~~~
`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.

63
enum.h
View File

@ -1190,6 +1190,69 @@ BETTER_ENUMS__CONSTEXPR inline bool operator >=(const Enum &a, const Enum &b) \
namespace better_enums {
// Maps.
template <typename T>
struct map_compare {
BETTER_ENUMS__CONSTEXPR static bool less(const T& a, const T& b)
{ return a < b; }
};
template <>
struct map_compare<const char*> {
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 <typename Enum, typename T, typename Compare = map_compare<T> >
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<Enum>
to_enum_nothrow(T value, size_t index = 0) const
{
return
index >= Enum::_size() ? optional<Enum>() :
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 <typename Enum, typename T>
BETTER_ENUMS__CONSTEXPR map<Enum, T> make_map(T (*f)(Enum))
{
return map<Enum, T>(f);
}
// Stream I/O operators.
// This template is used as a sort of enable_if for SFINAE. It should be

74
example/5-map.cc Normal file
View File

@ -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 <iostream>
#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 "for gcc 5.1";
}
// Here is the map. The actual type is better_enums::map<Channel, const char*>.
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<E, T>. The full
// signature of the template better_enums::map is
//
// template <typename Enum, typename T, typename Compare = map_compare<T>>
//
// 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.

View File

@ -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)

View File

@ -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

4
test/expect/5-map Normal file
View File

@ -0,0 +1,4 @@
the red channel
the red channel
Green
Blue