c++templatesc++20variadic-templatesc++-concepts

How to write a concept that checks for functions with variadic arguments?


I originally wanted to do this to create a concept that checks for allocators. I wanted to ensure that whatever template argument is passed, the following statement is correct:

std::allocator_traits::construct(alloc, ptr, ... /* parameters of constructors for AllocType::value_type */)

But here is a MRE:

#include <utility>
#include <iostream>
#include <concepts>

template <typename T>
struct Allocator {
    template <typename ...Args>
    void construct(Args&&... args) {
        T t { std::forward<Args>(args)... };
        t.print();
    }
};

struct X1 {
    int i;
    X1(int i) : i(i) { }
    void print() { std::cout << i << '\n'; }
};

struct X2 {
    int i;
    std::string s;
    X2(int i, std::string s) : i(i), s(std::move(s)) { }
    void print() { std::cout << i << " - " << s << '\n'; }
};

template <template <typename> typename Alloc, typename T, typename... Args>
concept can_construct = requires(Alloc<T> a, Args... args) {
    { T(args...) } -> std::same_as<T>;
    { a.construct(args...) } -> std::same_as<void>;
};

int main() {
    static_assert(can_construct<Allocator, X1, int>);
    static_assert(can_construct<Allocator, X2, int, std::string>);

    // These fail
    // static_assert(can_construct<Allocator, X2, int>);
    // static_assert(can_construct<Allocator, X1, int, std::string>);
}

This is the closest I've come to, which, in addition to not being clean at all, doesn't even do the job I wanted from it. This requires knowledge of T and checking every constructor parameter set. It also has the knowledge that inside construct, we actually call the constructor, but this could've been something different, such as calling some function with the passed variadic args. If it was, then in addition to the check { T(args...) } -> std::same_as<T>;, we'd need to check for { func(...) } -> ... as well.

Back to the first question, how would I be able to get two template args, Alloc and T, and check if Alloc::construct can be called with every set of args for all constructors of T?

In addition to that, how do we confirm that the set of passed template args don't cause compilation errors for a variadic template?


Solution

  • For an arbitrary type T, the set of types for which a type T can be constructed require solving Halt.

    Specifically, lets look at this toy type:

    struct Silly {
      template<std::size_t I> requires some_computation(I)
      Silly( int(&)[I] ) {}
    };
    

    Silly can only be constructed by arrays whose size passes the test some_computation. In order to figure out what sizes of array Silly can be constructed from, you have to invert the near-arbitrary function some_computation and work out which values return true.

    constexpr in C++ is Turing-complete; the problem of inverting arbitrary Turing-complete programs is at least as hard as Halt, and Halt cannot be solved.

    In this case, the compiler would "merely" have to check all 2^(8*sizeof(size_t)) (stay a while... stay forever!) possibilities, but it isn't hard to put an std::size_t...Is in there and make the problem truly unbounded.

    Now, the type I'm demonstrating is not intended to reflect a practical type -- but it is merely a simple case of an entire class of similar problems. The full set of argument types that can be used to construct a type is not something that can be computed in the general case.

    If you had something like reflection, you could probably get closer; you could solve some simpler sub-cases of the general problem. However, C++ does not currently have reflection, so you cannot even solve those simpler versions of this problem. Try back in or or maybe.

    C++ often has Turing-complete subsystems, as making a powerful subsystem not Turing-complete is amusingly difficult; these overly-strong subsystems mean that inverting them is impossible. A type can express its viable construction arguments in a way that requires a Turing-complete computation to work out if a set of arguments is valid; as such, finding all valid arguments is simply impossible in the general case.