Background: I'm writing a wrapper type like Either<A, B>, and I'd like return {some, args}; to work from a function returning Either<A, B> exactly when it'd work from a function returning A or B. However, I also want to detect when both A and B could be initialized with {some, args}, and produce an error to protect users from the ambiguity.
To detect whether a type T can be initialized from some arguments, I tried writing a function like this:
template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});
// imagine some other fallback overload here...
I thought the expression testInit<T>(some, args) should be valid exactly when T{some, args} is valid — in the code below, the initialization auto x = MyType{1UL, 'a'}; works, and this test also passes:
struct MyType {
MyType(size_t a, char b) {}
};
auto x = MyType{1UL, 'a'}; // ok
static_assert(std::is_same<MyType, decltype(testInit<MyType>(1UL, 'a'))>::value, ""); // ok
However, when we add a constructor from std::initializer_list<char>, it breaks:
struct MyType {
MyType(size_t a, char b) {}
MyType(std::initializer_list<char> x) {} // new!
};
auto x = MyType{1UL, 'a'}; // still ok
// FAILS:
static_assert(std::is_same<MyType, decltype(testInit<MyType>(1UL, 'a'))>::value, "");
note: candidate template ignored: substitution failure [with T = MyType, Args = <unsigned long, char>]: non-constant-expression cannot be narrowed from type 'unsigned long' to 'char' in initializer listauto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...}); ^ ~~~
Why is Clang refusing to resolve my (size_t, char) constructor in favor of the initializer_list constructor? How can I correctly detect whether return {some, args}; would work in a function returning T, regardless of whether it's an aggregate type, a user-defined constructor, or an initializer_list constructor?