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?
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 c++26 or c++29 or c++32 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.