如何检测类型是否可列表初始化?

jtb*_*des 5 c++ templates narrowing variadic-templates c++11

背景:我正在编写一个包装器类型,例如Either<A, B>,并且我想return {some, args};从返回的函数中工作,Either<A, B>当它从返回的函数中工作时,AB. 不过,我也希望,当检测到两个 AB可能与初始化{some, args},并产生一个错误,从模糊保护用户。

为了检测是否T可以从某些参数初始化类型,我尝试编写这样的函数:

template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});

// imagine some other fallback overload here...
Run Code Online (Sandbox Code Playgroud)

我认为表达式testInit<T>(some, args)应该在有效的时候T{some, args}有效——在下面的代码中,初始化auto x = MyType{1UL, 'a'};工作,并且这个测试也通过了:

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
Run Code Online (Sandbox Code Playgroud)

但是,当我们从 中添加构造函数时std::initializer_list<char>,它会中断:

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, "");
Run Code Online (Sandbox Code Playgroud)

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)...});
     ^                                      ~~~
Run Code Online (Sandbox Code Playgroud)

为什么 Clang 拒绝解析我的(size_t, char)构造函数而支持initializer_list构造函数?如何正确检测是否return {some, args};可以在返回的函数中工作T,无论它是聚合类型、用户定义的构造函数还是initializer_list构造函数?

max*_*x66 3

有点复杂。

我并不是真正的专家,所以我可以说一些不完全准确的话:对我所说的话持保留态度。

首先:当你写的时候

auto x = MyType{1UL, 'a'};  // ok
Run Code Online (Sandbox Code Playgroud)

调用的构造函数是初始化列表之一,而不是接收 astd::size_tchar.

这是有效的,因为第一个值1UL是一个unsigned long intbut ,其值(注意:a value)可以缩小到char。也就是说:有效,因为1UL是一个适合 a 的值char

如果你试试

auto y = MyType{1000UL, 'a'};  // ERROR!
Run Code Online (Sandbox Code Playgroud)

您会收到错误,因为1000UL无法缩小到char. 也就是说:1000UL不适合 a char

这也适用于decltype()

decltype( char{1UL} )    ch1; // compile
decltype( char{1000UL} ) ch2; // ERROR
Run Code Online (Sandbox Code Playgroud)

但考虑一下这个功能

auto test (std::size_t s)
   -> decltype( char{s} );
Run Code Online (Sandbox Code Playgroud)

该函数立即给出编译错误。

你可以这样想:“但是如果传递1ULtest()decltype()可以将std::size_t值缩小到 a char

问题在于 C 和 C++ 是强类型语言;如果您允许test(), 有效,返回一个类型,当收到 的某些值时std::size_t,您可以创建(通过 SFINAE)一个函数,该函数返回某些值的类型和另一种类型的另一种类型。从强类型语言的角度来看,这是不可接受的。

所以

auto test (std::size_t s)
   -> decltype( char{s} );
Run Code Online (Sandbox Code Playgroud)

decltype( char{s} )当 的所有可能值都可接受时才可接受s。也就是说:test()是不可接受的,因为std::size_t可以容纳1000UL不适合 的内容char

现在做一点改变:制作test()一个模板函数

template <typename T>
auto test (T s)
   -> decltype( char{s} );
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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)...});
Run Code Online (Sandbox Code Playgroud)

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 (...);
Run Code Online (Sandbox Code Playgroud)

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, "!"); 
Run Code Online (Sandbox Code Playgroud)

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!
Run Code Online (Sandbox Code Playgroud)

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 (...);
Run Code Online (Sandbox Code Playgroud)

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, "!"); 
Run Code Online (Sandbox Code Playgroud)