jtb*_*des 5 c++ templates narrowing variadic-templates c++11
背景:我正在编写一个包装器类型,例如Either<A, B>,并且我想return {some, args};从返回的函数中工作,Either<A, B>当它从返回的函数中工作时,A或B. 不过,我也希望,当检测到两个 A及B可能与初始化{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 listRun Code Online (Sandbox Code Playgroud)auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...}); ^ ~~~
为什么 Clang 拒绝解析我的(size_t, char)构造函数而支持initializer_list构造函数?如何正确检测是否return {some, args};可以在返回的函数中工作T,无论它是聚合类型、用户定义的构造函数还是initializer_list构造函数?
有点复杂。
我并不是真正的专家,所以我可以说一些不完全准确的话:对我所说的话持保留态度。
首先:当你写的时候
auto x = MyType{1UL, 'a'}; // ok
Run Code Online (Sandbox Code Playgroud)
调用的构造函数是初始化列表之一,而不是接收 astd::size_t和char.
这是有效的,因为第一个值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)
该函数立即给出编译错误。
你可以这样想:“但是如果传递1UL给test(),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)