简体   繁体   中英

Multiple concept-constrained parameter packs in variadic function in C++20 does not accept arguments in the first parameter pack

The following code uses multiple parameter packs to define a variadic template accumulator function that accepts any numeric type or pointer to numeric type:

// Compile with --std=c++20

#include <type_traits>

template <typename T>
concept number = std::is_arithmetic_v<T>
             && !std::is_pointer_v<T>;

template <typename T>
concept pointer = std::is_arithmetic_v<std::remove_pointer_t<T>>
               && std::is_pointer_v<T>;


double foo ()
{return 0;}

double foo (pointer auto     p0)
{return *p0;}

double foo (pointer auto     p0,
            pointer auto ... ps)
{return *p0 + foo (ps ...);}

double foo (number  auto     n0,
            pointer auto ... ps)
{return n0 + foo (ps ...);}

double foo (number  auto     n0,
            number  auto ... ns,  /* <---- THIS LINE */
            pointer auto ... ps)
{return n0 + foo (ns ..., ps ...);}


int main()
{
    float f = 3.;
    unsigned u = 4;
    
    foo (); // Compiles
    foo (1); // Compiles
    foo (&f); // Compiles
    foo (1, &f); // Compiles
    foo (1, &f, &u); // Compiles
    foo (&f, &u); // Compiles
    
    foo (1, 2.); // Error!
    foo (1, 2., &f); // Error!
    foo (1, 2., &f, &u); // Error!
}

The error triggers when there is more than one argument of type number .

It looks like when there are multiple parameter packs, the compiler packs all arguments in the last pack, instead of refering to the constraints to define which argument belongs to which parameter pack.

Is this a limitation of the language? Are multiple parameter packs meant to be used in some other way? Is there any workaround to make it work?

tested in clang and GCC

Update: Solved!

Solution: Use a single parameter pack, do not constrain the parameter pack, and constrain the type of the parameters on a one-by-one basis.

// Compile with --std=c++20

#include <type_traits>

template <typename T>
concept number = std::is_arithmetic_v<T>;

template <typename T>
concept pointer = std::is_arithmetic_v<std::remove_pointer_t<T>>
               && std::is_pointer_v<T>;


double foo ()
{return 0;}

double foo (pointer auto     p0)
{return *p0;}

double foo (pointer auto     p0,
            pointer auto ... ps)
{return *p0 + foo (ps ...);}

template <typename ... N_P>
double foo (number auto n0,
            N_P ... ps)
{return n0 + foo (ps ...);}


int main()
{
    float f = 3.;
    unsigned u = 4;
    
    foo (); // Compiles
    foo (1); // Compiles
    foo (&f); // Compiles
    foo (1, &f); // Compiles
    foo (1, &f, &u); // Compiles
    foo (&f, &u); // Compiles
    
    foo (1, 2.); // Good!
    foo (1, 2., &f); // Good!
    // foo (1, &f, 2.); // Does not compile (Good!)
    return foo (1, 2., &f, &u); // Good!
}

Deduction for function parameter packs only occurs for the last pack in the argument list. All other packs are considered a non-deduced context :

The non-deduced contexts are:

...

A function parameter pack that does not occur at the end of the parameter-declaration-list.

Concepts doesn't affect this. You can't use concepts as a way to make the first pack deducible.

In any case, it's much easier to just have a concept that could be an arithmetic type or a pointer to an arithmetic type, a fold expression, and a single function to distinguish which from which :

#include <type_traits>

template <typename T>
concept number = std::is_arithmetic_v<T>; //Pointers aren't arithmetic types.

template <typename T>
concept ptr_to_num =
    std::is_pointer_v<T> &&
    number<std::remove_pointer_t<T>>;

template<typename T>
concept ptr_to_num_or_num =
    number<T> || ptr_to_num<T>;

template<ptr_to_num_or_num T>
double dereference(T p)
{
    if constexpr(ptr_to_num<T>)
        return *p;
    else
        return p;
}

template<ptr_to_num_or_num ...Args>
double foo(Args ...args)
{
    return (0.0 + ... + dereference(args));
}

int main()
{
    float f = 3.;
    unsigned u = 4;
    
    foo (); // Compiles
    foo (1); // Compiles
    foo (&f); // Compiles
    foo (1, &f); // Compiles
    foo (1, &f, &u); // Compiles
    foo (&f, &u); // Compiles
    
    foo (1, 2.); // Error!
    foo (1, 2., &f); // Error!
    foo (1, 2., &f, &u); // Error!
}

Yes, you will be able to pass pointers before numbers. But isn't that a better interface for whatever you're trying to do?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM