Concise bidirectional static 1:1 mapping of values and types

As @yeputons said, friend-injection can help here. It's a spooky feature, and I can't say I fully understand how it works, but here it goes.

#include <iostream>
#include <type_traits>

template <typename T>
struct tag {using type = T;};

template <typename T>
struct type_to_enum_friend_tag
{
    friend constexpr auto adl_type_to_enum(type_to_enum_friend_tag);
};
template <auto E>
struct enum_to_type_friend_tag
{
    friend constexpr auto adl_enum_to_type(enum_to_type_friend_tag);
};

namespace impl
{
    // Would've used `= delete;` here, but GCC doesn't like it.
    void adl_type_to_enum() {}
    void adl_enum_to_type() {}
}

template <typename T>
constexpr auto type_to_enum_helper()
{
    // Make sure our ADL works even if some stray
    // identifier named `adl_type_to_enum` is visible.
    using impl::adl_type_to_enum;
    return adl_type_to_enum(type_to_enum_friend_tag<T>{});
}
template <typename T>
inline constexpr auto type_to_enum = type_to_enum_helper<T>();

template <auto E>
constexpr auto enum_to_type_helper()
{
    // Make sure our ADL works even if some stray
    // identifier named `adl_type_to_enum` is visible.
    using impl::adl_enum_to_type;
    return adl_enum_to_type(enum_to_type_friend_tag<E>{});
}
template <auto E>
using enum_to_type = typename decltype(enum_to_type_helper<E>())::type;


template <typename T, auto E>
struct foo
{
    friend constexpr auto adl_type_to_enum(type_to_enum_friend_tag<T>)
    {
        return E;
    }
    friend constexpr auto adl_enum_to_type(enum_to_type_friend_tag<E>)
    {
        return tag<T>{};
    }
};

enum class foo_type {bar = 42};
struct bar : foo<bar, foo_type::bar>
{
    void say() {std::cout << "I'm bar!\n";}
};

int main()
{
    std::cout << int(type_to_enum<bar>) << '\n'; // 42
    enum_to_type<foo_type::bar>{}.say(); // I'm bar!
}

Run on gcc.godbolt.org

It appears to work on both GCC, Clang, and MSVC.

I'm using an auto template parameter, so you can map different types to constants from different enums, or even to plain integers. Constraining this to accept only a single specific enum should be easy, and is left as an exercise to the reader.


Of course, for the type-to-enum mapping you could simply add a static constexpr member variable to foo. But I don't know any good alternatives to friend-injection for the enum-to-type mapping.


@HolyBlackCat's answer is fantastic. Type-to-enum can be achieved in simpler ways than ADL hackery, so I tried to distil the enum-to-type bit to the bare minimum:

template <auto E>
struct adl_to_type 
{
    friend auto foo_type_to_type(adl_to_type);
};

template<typename T, foo_type E>
struct foo 
{
    friend auto foo_type_to_type(adl_to_type<E>) { return (T*)nullptr; };
};

template <foo_type E>
using to_type = std::remove_pointer_t<decltype(foo_type_to_type(adl_to_type<E>{}))>;

int main() 
{
    to_type<foo_type::bar>{}.say();
    return 0; 
}

Run on gcc.godbolt.org

It still blows my mind. The auto return type is absolutely crucial here. Even changing it to T* in foo will yield a compile error. I also tried with getting rid of adl_to_type and using integral_constant instead, but it seems that declaring foo_type_to_type as the friend function inside the type used to resolve ADL is the key here.