Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concise bidirectional static 1:1 mapping of values and types

I'm going to start with how I imagine using the code I'd like to create. It doesn't have to be exactly like this but it's a good example of what I mean by "concise" in the title. In my case it's mapping of a type to a related enumeration value.

struct bar : foo<bar, foo_type::bar> { /* ... */ };
//               \_/  \___________/
//                ^ Type         ^ Value

What this should ideally do is an automatic registration of a bidirectional mapping between the first template parameter of foo, a type, and second, a value, just with the inheritance syntax and proper template parameters, so that I can later do what's in the example below.

foo_type value = to_value<bar>; // Should be foo_type::bar
using type = to_type<foo_type::bar>; // Should be bar

I know I could manually write two template specializations per type-value pair to do this but I'm wondering if it could be less tedious than that without using macros.

What I tried already is...

  1. Specializing template aliases to write less code to generate specializations. Apparently not possible in the current C++ version (17/20).
  2. Specializing inherited template member types.
struct foo_base
{
    template<typename T>
    struct to_value
    {};

    template<foo_type E>
    struct to_type
    {};
};

template<typename T, foo_type E>
struct foo : public foo_base
{
    template<>
    struct to_value<T>
    {
        static constexpr auto value = E;
    };

    template<>
    struct to_type<E>
    {
        using type = T;
    };
};

It would then be used similarily to what I presented at the beginning.

foo_type value = foo_base::to_value<bar>::value; // Should be foo_type::bar
using type = foo_base::to_type<foo_type::bar>::type; // Should be bar

But it fails with errors below on MSVC.

explicit specialization; 'foo_base::to_value' has already been instantiated

'foo_base::to_value': cannot specialize template in current scope

I feel like it might not be doable without explicit manual specializations, but C++17 allows a lot of surprising template based hacks, so want to confirm with more experienced people before I drop the idea.

like image 243
Vennor Avatar asked Jun 18 '20 11:06

Vennor


2 Answers

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.

like image 112
HolyBlackCat Avatar answered Nov 12 '22 03:11

HolyBlackCat


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

like image 28
gwiazdorrr Avatar answered Nov 12 '22 02:11

gwiazdorrr