Divorce a parameter pack in a class template

大憨熊 提交于 2020-07-15 06:11:56

问题


I am trying to write a class template that uses a parameter-pack and implements a member function for each type contained in the parameter-pack.

This is what I have so far:

template <typename...T>
class Myclass {
public:
    void doSomething((Some_Operator_to_divorce?) T) {
        /*
         * Do Something
         */
        std::cout << "I did something" << std::endl;
    }
};

My goal is to have a class template that can be used in the following way:

Myclass<std::string, int, double> M;

M.doSomething("I am a String");
M.doSomething(1234);
M.doSomething(0.1234);

Where the class template mechanism will create an implementation for a doSomething(std::string x), a doSomething(int x) and a doSomething(double x) member function but not a doSomething(std::string x, int i, double f) member function.

I found a lot of examples in the web on the usability of parameter-packs, but I could not figure out if it can be used for my purpose, or if I totally misunderstood for what a parameter-pack can be used.

I thought that I need to unpack the parameter-pack but, after reading a lot of examples about unpacking parameter packs, I believe that this is not the right choice and it has a complete different meaning.

So, therefore, I am looking for a operation to "divorce" a parameter-pack.


回答1:


There is no "operator" specifically that supports this, but what you're requesting can be done in a few different ways, depending on your requirements.

The only way to "extract" T types from a parameter pack of a class template with the purpose of implementing an overload-set of functions is to implement it using recursive inheritance, where each instance extracts one "T" type and implements the function, passing the rest on to the next implementation.

Something like:

// Extract first 'T', pass on 'Rest' to next type
template <typename T, typename...Rest>
class MyClassImpl : public MyClassImpl<Rest...>
{
public:
    void doSomething(const T&) { ... }
    using MyClassImpl<Rest...>::doSomething;
};

template <typename T>
class MyClassImpl<T> // end-case, no more 'Rest'
{
public:
    void doSomething(const T&) { ... }
};

template <typename...Types>
class MyClass : public MyClassImpl<Types...>
{
public:
    using MyClassImpl<Types...>::doSomething;
    ...
};

This will instantiate sizeof...(Types) class templates, where each one defines an overload for each T type.

This ensures that you get overload semantics -- such that passing an int can call a long overload, or will be ambiguous if there are two competing conversions.

However, if this is not necessary, then it'd be easier to enable the function with SFINAE using enable_if and a condition.

For exact comparisons, you could create an is_one_of trait that only ensures this exists if T is exactly one of the types. In C++17, this could be done with std::disjunction and std::is_same:

#include <type_traits>

// A trait to check that T is one of 'Types...'
template <typename T, typename...Types>
struct is_one_of : std::disjunction<std::is_same<T,Types>...>{};

Alternatively, you may want this to only work if it may work with convertible types -- which you might do something like:

template <typename T, typename...Types>
struct is_convertible_to_one_of : std::disjunction<std::is_convertible<T,Types>...>{};

The difference between the two is that if you passed a string literal to a MyClass<std::string>, it will work with the second option since it's convertible, but not the first option since it's exact. The deduced T type from the template will also be different, with the former being exactly one of Types..., and the latter being convertible (again, T may be const char*, but Types... may only contain std::string)

To work this together into your MyClass template, you just need to enable the condition with SFINAE using enable_if:

template <typename...Types>
class MyClass
{
public:

    // only instantiates if 'T' is exactly one of 'Types...'
    template <typename T, typename = std::enable_if_t<is_one_of<T, Types...>::value>>
    void doSomething(const T&) { ... }

    // or 

    // only instantiate if T is convertible to one of 'Types...'
    template <typename T, typename = std::enable_if_t<is_convertible_to_one_of<T, Types...>::value>>
    void doSomething(const T&) { ... }
};

Which solution works for you depends entirely on your requirements (overload semantics, exact calling convension, or conversion calling convension)


Edit: if you really wanted to get complex, you can also merge the two approaches... Make a type trait to determine what type would be called from an overload, and use this to construct a function template of a specific underlying type.

This is similar to how variant needs to be implemented, since it has a U constructor that considers all types as an overload set:

    // create an overload set of all functions, and return a unique index for
    // each return type
    template <std::size_t I, typename...Types>
    struct overload_set_impl;

    template <std::size_t I, typename T0, typename...Types>
    struct overload_set_impl<I,T0,Types...>
      : overload_set_impl<I+1,Types...>
    {
      using overload_set_impl<I+1,Types...>::operator();

      std::integral_constant<std::size_t,I> operator()(T0);
    };

    template <typename...Types>
    struct overload_set : overload_set_impl<0,Types...> {};

    // get the index that would be returned from invoking all overloads with a T
    template <typename T, typename...Types>
    struct index_of_overload : decltype(std::declval<overload_set<Types...>>()(std::declval<T>())){};

    // Get the element from the above test
    template <typename T, typename...Types>
    struct constructible_overload
      : std::tuple_element<index_of_overload<T, Types...>::value, std::tuple<Types...>>{};

    template <typename T, typename...Types>
    using constructible_overload_t
      = typename constructible_overload<T, Types...>::type;

And then use this with the second approach of having a function template:

template <typename...Types>
class MyClass {
public:
    // still accept any type that is convertible
    template <typename T, typename = std::enable_if_t<is_convertible_to_one_of<T, Types...>::value>>
    void doSomething(const T& v) 
    {
        // converts to the specific overloaded type, and call it
        using type = constructible_overload_t<T, Types...>;
        doSomethingImpl<type>(v); 
    }
private:
    template <typename T>
    void doSomethingImpl(const T&) { ... }

This last approach does it two-phase; it uses the first SFINAE condition to ensure it can be converted, and then determines the appropriate type to treat it as and delegates it to the real (private) implementation.

This is much more complex, but can achieve the overload-like semantics without actually requiring recursive implementation in the type creating it.



来源:https://stackoverflow.com/questions/62578701/divorce-a-parameter-pack-in-a-class-template

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!