Search code examples
c++templatesvariadic-templatesc++17c++-faq

Restrict variadic template arguments


Can we restrict variadic template arguments to a certain type? I.e., achieve something like this (not real C++ of course):

struct X {};

auto foo(X... args)

Here my intention is to have a function which accepts a variable number of X parameters.

The closest we have is this:

template <class... Args>
auto foo(Args... args)

but this accepts any type of parameter.


Solution

  • Yes it is possible. First of all you need to decide if you want to accept only the type, or if you want to accept a implicitly convertible type. I use std::is_convertible in the examples because it better mimics the behavior of non-templated parameters, e.g. a long long parameter will accept an int argument. If for whatever reason you need just that type to be accepted, replace std::is_convertible with std:is_same (you might need to add std::remove_reference and std::remove_cv).

    Unfortunately, in C++ narrowing conversion e.g. (long long to int and even double to int) are implicit conversions. And while in a classical setup you can get warnings when those occur, you don't get that with std::is_convertible. At least not at the call. You might get the warnings in the body of the function if you make such an assignment. But with a little trick we can get the error at the call site with templates too.

    So without further ado here it goes:


    The testing rig:

    struct X {};
    struct Derived : X {};
    struct Y { operator X() { return {}; }};
    struct Z {};
    
    foo_x : function that accepts X arguments
    
    int main ()
    {
       int i{};
       X x{};
       Derived d{};
       Y y{};
       Z z{};
       
       foo_x(x, x, y, d); // should work
       foo_y(x, x, y, d, z); // should not work due to unrelated z
    };
    

    C++20 Concepts

    Not here yet, but soon. Available in gcc trunk (March 2020). This is the most simple, clear, elegant and safe solution:

    #include <concepts>
    
    auto foo(std::convertible_to<X> auto ... args) {}
    
    foo(x, x, y, d); // OK
    foo(x, x, y, d, z); // error:
    

    We get a very nice error. Especially the

    constraints not satisfied

    is sweet.

    Dealing with narrowing:

    I didn't find a concept in the library so we need to create one:

    template <class From, class To>
    concept ConvertibleNoNarrowing = std::convertible_to<From, To>
        && requires(void (*foo)(To), From f) {
            foo({f});
    };
    
    auto foo_ni(ConvertibleNoNarrowing<int> auto ... args) {}
    
    foo_ni(24, 12); // OK
    foo_ni(24, (short)12); // OK
    foo_ni(24, (long)12); // error
    foo_ni(24, 12, 15.2); // error
    

    C++17

    We make use of the very nice fold expression:

    template <class... Args,
             class Enable = std::enable_if_t<(... && std::is_convertible_v<Args, X>)>>
    auto foo_x(Args... args) {}
    
    foo_x(x, x, y, d, z);    // OK
    foo_x(x, x, y, d, z, d); // error
    

    Unfortunately we get a less clear error:

    template argument deduction/substitution failed: [...]

    Narrowing

    We can avoid narrowing, but we have to cook a trait is_convertible_no_narrowing (maybe name it differently):

    template <class From, class To>
    struct is_convertible_no_narrowing_impl {
      template <class F, class T,
                class Enable = decltype(std::declval<T &>() = {std::declval<F>()})>
      static auto test(F f, T t) -> std::true_type;
      static auto test(...) -> std::false_type;
    
      static constexpr bool value =
          decltype(test(std::declval<From>(), std::declval<To>()))::value;
    };
    
    template <class From, class To>
    struct is_convertible_no_narrowing
        : std::integral_constant<
              bool, is_convertible_no_narrowing_impl<From, To>::value> {};
    

    C++14

    We create a conjunction helper:
    please note that in C++17 there will be a std::conjunction, but it will take std::integral_constant arguments

    template <bool... B>
    struct conjunction {};
    
    template <bool Head, bool... Tail>
    struct conjunction<Head, Tail...>
        : std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
    
    template <bool B>
    struct conjunction<B> : std::integral_constant<bool, B> {};
    

    and now we can have our function:

    template <class... Args,
              class Enable = std::enable_if_t<
                  conjunction<std::is_convertible<Args, X>::value...>::value>>
    auto foo_x(Args... args) {}
    
    
    foo_x(x, x, y, d); // OK
    foo_x(x, x, y, d, z); // Error
    

    C++11

    just minor tweaks to the C++14 version:

    template <bool... B>
    struct conjunction {};
    
    template <bool Head, bool... Tail>
    struct conjunction<Head, Tail...>
        : std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
    
    template <bool B>
    struct conjunction<B> : std::integral_constant<bool, B> {};
    
    template <class... Args,
              class Enable = typename std::enable_if<
                  conjunction<std::is_convertible<Args, X>::value...>::value>::type>
    auto foo_x(Args... args) -> void {}
    
    foo_x(x, x, y, d); // OK
    foo_x(x, x, y, d, z); // Error