Concise bidirectional static 1:1 mapping of values and types
Asked Answered
N

2

6

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.

Naphthyl answered 18/6, 2020 at 11:20 Comment(6)
what is foo_type ?Bacteriostasis
It's a scoped enum in my case. I added this detail in one of the edits.Naphthyl
to_value is easy: just make it look inside bar (you can add arbitrary symbols here in the foo wrapper).Tori
Maybe friend functions would be of some use, they allow creepy stuff.Tori
There are going to be more derived types than just bar though.Naphthyl
Ah, that's not a problem actually if value is defined as a static member of templated foo.Naphthyl
C
4

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.

Coquina answered 18/6, 2020 at 12:21 Comment(3)
Is not it an exploit of core language issue #2118?Dagny
@Dagny Yep, this is what CWG 2118 is about. As of now, the standard seems to say that it's well-formed. CWG said they don't like this feature, but they can say anything they want. :P I doubt they'll find a way (or dare) to remove it from the language without breaking some well-behaved programs.Coquina
For stateful template metaprogramming, this is somewhat tame and thus more reliable, compared to things like unconstexpr (which only worked for a single specific version of GCC).Coquina
H
4

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

Histrionic answered 18/6, 2020 at 13:17 Comment(1)
The extra ADL hackery is needed to prevent ADL from breaking if user does something like namespace A {int foo_type_to_type;} using namespace A;. An unlikely scenario, but I wouldn't want to debug this sorcery if it ever breaks. :)Coquina

© 2022 - 2024 — McMap. All rights reserved.