Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I detect whether a type is list-initializable?

Tags:

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?

like image 635
jtbandes Avatar asked Nov 17 '17 04:11

jtbandes


People also ask

How do you initialize a list in C++?

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.

What is std :: Initializer_list?

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 .

What is uniform initialization in C++?

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.


1 Answers

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, "!"); 
like image 164
max66 Avatar answered Sep 20 '22 13:09

max66