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 list
auto 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?
When do we use Initializer List in C++? Initializer List is used in initializing the data members of a class. The list of members to be initialized is indicated with constructor as a comma-separated list followed by a colon. Following is an example that uses the initializer list to initialize x and y of Point class.
std::initializer_list This type is used to access the values in a C++ initialization list, which is a list of elements of type const T .
Uniform initialization is a feature in C++ 11 that allows the usage of a consistent syntax to initialize variables and objects ranging from primitive type to aggregates. In other words, it introduces brace-initialization that uses braces ({}) to enclose initializer values.
It's a little complicated.
And I'm not really an expert so I can say something non completely exact: take what I say with a pinch of salt.
First of all: when you write
auto x = MyType{1UL, 'a'}; // ok
the constructor called is the initializer list one, not the one that receive a std::size_t
and the char
.
And this works because the first value, 1UL
is an unsigned long int
but with a value (attention: a value) that can be narrowed to char
. That is: works because 1UL
is a value that fits in a char
.
If you try
auto y = MyType{1000UL, 'a'}; // ERROR!
you get an error because 1000UL
can't be narrowed to a char
. That is: the 1000UL
doesn't fit in a char
.
This works also with decltype()
:
decltype( char{1UL} ) ch1; // compile
decltype( char{1000UL} ) ch2; // ERROR
But consider this function
auto test (std::size_t s)
-> decltype( char{s} );
This function gives immediately a compilation error.
You can think: "but if a pass 1UL
to test()
, decltype()
can narrow the std::size_t
value to a char
"
The problem is that C and C++ are strongly typed languages; if you permit that test()
, works, returning a type, when receive some values of std::size_t
, you can create (via SFINAE) a function that return a type for some values and another type with another types. This in unacceptable from the point of view of a strongly typed language.
So
auto test (std::size_t s)
-> decltype( char{s} );
is acceptable only if decltype( char{s} )
is acceptable for all possible values of s
. That is: test()
is unacceptable because std::size_t
can hold the 1000UL
that doesn't fit in a char
.
A little change now: make test()
a template function
template <typename T>
auto test (T s)
-> decltype( char{s} );
Now test()
compile; because there are types T
with all values that can be narrowed to a char
(T = char
, by example). So test()
, templatized, isn't intrinsically wrong.
But when you use it with a std::size_t
decltype( test(1UL) ) ch; // ERROR
you get an error because test()
can't accept a std::size_t
. Neither a value that can be narrowed to a char
.
This is exactly the problem of your code.
Your testInit()
template <typename T, typename... Args>
auto testInit(Args&&... args)
-> decltype(T{std::forward<Args>(args)...});
is acceptable because there are types T
and Args...
so that T{std::forward<Args>(args)...}
is acceptable (example: T = int
and Args... = int
).
But T = MyType
and Args... = std::size_t, char
is unacceptable because the constructor used is the one with an initializer list of char
and non all std::size_t
values can be narrowed to a char
.
Concluding: you get an error compiling decltype(testInit<MyType>(1UL, 'a')
because you get an error compiling MyType{1000UL, 'a'}
.
Bonus answer: I suggest an improvement (IMHO) for your testInit()
.
Using SFINAE and the power of the comma operator, you can write
template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T{ args... }, std::true_type{} );
template <typename...>
std::false_type testInit (...);
So you can write some static_assert()
simply as follows
static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!");
static_assert( false == decltype(testInit<MyType>(1UL, 'b'))::value, "!");
Post scriptum: if you want that the MyType(size_t a, char b) {}
constructor is called, you can use the round parentheses
auto y = MyType(1000UL, 'a'); // compile!
So if you write testInit()
with round parentheses
template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T( args... ), std::true_type{} );
template <typename...>
std::false_type testInit (...);
you pass both following static_assert()
s
static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!");
static_assert( true == decltype(testInit<MyType>(1UL, 'b'))::value, "!");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With