Search code examples
c++templatesc++20variadic-templatesc++-concepts

Variadic Templates with Concepts


I've tested variadic templates inside a function, which I developed step by step.

template <typename... Ts>
void foo (Ts...) noexcept {}

// Test code
int i {123};
int j {456};
foo(1,2,3,i,j);

It compiles.

Then I added a forwarding reference.

template <typename... Ts>
void foo (Ts&&...) noexcept {}

// Test code
int i {123};
int j {456};
foo(1,2,3,i,j);

This still compiles. At last, I decided to add a concept to contrain the function to work only with numbers.

template <typename T>
concept MyNumber =
  (is_integral_v<T> || is_floating_point_v<T>);

template <MyNumber... Ts>
void foo (Ts&&...) noexcept {}

// Test code
int i {123};
int j {456};
foo(1,2,3,i,j); // does not compile
foo(1,2,3); // compiles

It does NOT compile. The solution would be to remove && in foo's signature, or i and j to pass with std::forward. What is here the best practice? The usage of foo should be easy as possible.


Solution

  • The problem with forwarding references T&& is that

    • if you pass an lvalue of type int (e.g. i) to the function,
    • if you pass an rvalue of type int (e.g. 0) to the function,
      • T deduces to int, and
      • T&& is simply int&&

    In other words, the Ts in your variadic function template are sometimes references int&, and sometimes the type int itself. is_integral_v<T> is not true when T is a reference because e.g. int& is not an integral type, it is a reference.

    Solution A - Use values instead of forwarding references

    template <MyNumber... Ts>
    void foo(Ts...) noexcept {}
    // or with abbreviated function templates:
    void foo(MyNumber auto...) noexcept {}
    

    This is perfectly fine and actually better than using forwarding references because MyNumber is only satisfied by small fundamental types like int and float that you should better pass by value anyway.

    Personally, I think this solution is more elegant then the alternative:

    Solution B - Remove references when checking constraints

    template <typename... Ts>
      requires ((MyNumber<std::remove_cvref_t<Ts>> && ...))
    void foo(Ts&&...) noexcept {}
    

    This would remove the & from int& when you're passing foo(i) for the purpose of checking whether it satisfies MyNumber.

    Note on the MyNumber concept

    Ideally, concepts should be built from other concepts so that some concepts can be made more constrained than others:

    #include <concepts>
    
    template <typename T>
    concept MyNumber = std::integral<T> || std::floating_point<T>;
    

    Only use type traits like std::is_integral_v when there is no equivalent concept in the standard library.