Search code examples
c++variadic-templatesvariadic-functionsenable-if

Is there a good way to enforce type restrictions on function parameters in a variadic template in C++?


I have an enum, let's call it Type. It has values as such:

enum Type { STRING, TYPE_A_INT, TYPE_B_INT};

I want to write a function Foo that can take arbitrarily many values of type {int, string}, but enforce that the template params match the param types.

Ideally it would behave as:

Foo<STRING, TYPE_A_INT>("str", 32); // works
Foo<STRING, TYPE_B_INT>("str", 32);  // works
Foo<STRING, TYPE_B_INT, TYPE_A_INT, STRING>("str", 32, 28, "str");  // works
Foo<STRING, TYPE_B_INT>("str", "str");  // doesn't compile

Is there a way to do this?

It seems like I could do something like the following, but this wouldn't work since Args would be Type and args would be {string, int}.

template<typename Arg, typename... Args>
std::enable_if<(std::is_same<Arg, STRING>::value)> 
Foo(String arg, Args... args) {
    // Do stuff to arg, then make recursive call.
    Foo(args);
}

template<typename Arg, typename... Args>
std::enable_if<(std::is_same<Arg, TYPE_A_INT>::value)> 
Foo(int arg, Args... args) {
    // Do stuff to arg, then make recursive call.
    Foo(args);
}

I could just wrap the arguments in something like

pair<Type, string>
pair<Type, int>

but it would be very nice to avoid this.


Solution

  • One easy way to do it would be to create a mapping from the enumerators to the desired types and use that to build the function parameter list - you could think of it as "enumerator traits", I guess:

    #include <iostream>
    #include <string>
    
    enum Type {STRING, TYPE_A_INT, TYPE_B_INT};
    
    template<Type> struct type_from;
    
    template<> struct type_from<STRING> { using type = std::string; };
    template<> struct type_from<TYPE_A_INT> { using type = int; };
    template<> struct type_from<TYPE_B_INT> { using type = int; };
    
    template<Type E> using type_from_t = typename type_from<E>::type;
    
    template<Type... Es> void Foo(type_from_t<Es>... args)
    {
       // Do stuff with args.
       using expander = int[];
       (void)expander{0, (std::cout << args << ' ', 0)...};
       std::cout << '\n';
    }
    
    int main()
    {
       Foo<STRING, TYPE_A_INT>("str", 32); // works
       Foo<STRING, TYPE_B_INT>("str", 32);  // works
       Foo<STRING, TYPE_B_INT, TYPE_A_INT, STRING>("str", 32, 28, "str");  // works
       // Foo<STRING, TYPE_B_INT>("str", "str");  // doesn't work
    }
    

    If you uncomment the last line, you'll get a nice error message telling you exactly what argument causes the problem.

    Of course, this doesn't ensure that the function argument types are exactly the ones given by the enumerator traits, but rather that there's a valid implicit conversion for each of them. As far as I understand, this is what you want, since your example passes string literals to std::strings.