背景:我正在编写一个类似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...
我认为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
但是,当我们从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, "");
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)...}); ^ ~~~
为什么Clang拒绝解析我的(size_t, char)
构造函数以支持initializer_list
构造函数? 如何正确检测return {some, args};
是否可以在返回T
的函数中工作,无论它是聚合类型,用户定义的构造函数还是{{1}构造函数?
答案 0 :(得分:1)
这有点复杂。
而且我并不是真正的专家,所以我可以说一些不完全准确的话:用一点点盐来说我说的话。
首先:写作时
auto x = MyType{1UL, 'a'}; // ok
调用的构造函数是初始化列表,而不是接收std::size_t
和char
的构造函数。
这是有效的,因为第一个值1UL
是unsigned long int
,但有一个值(注意:值),可以缩小到char
。这是:有效,因为1UL
是一个适合char
。
如果您尝试
auto y = MyType{1000UL, 'a'}; // ERROR!
您收到错误,因为1000UL
无法缩小为char
。也就是说:1000UL
不适合char
。
这也适用于decltype()
:
decltype( char{1UL} ) ch1; // compile
decltype( char{1000UL} ) ch2; // ERROR
但请考虑这个功能
auto test (std::size_t s)
-> decltype( char{s} );
此函数立即产生编译错误。
您可以认为:&#34;但如果传递1UL
到test()
,decltype()
可以将std::size_t
值缩小为char
&# 34;
问题是C和C ++是强类型语言;如果您允许test()
,工作,返回类型,当收到std::size_t
的某些值时,您可以创建(通过SFINAE)一个函数,该函数返回某些值的类型和另一种类型的其他类型。从强类型语言的角度来看,这是不可接受的。
所以
auto test (std::size_t s)
-> decltype( char{s} );
仅当decltype( char{s} )
对s
的所有可能值均可接受时,才可接受。这是:test()
是不可接受的,因为std::size_t
可以容纳1000UL
中不适合的char
。
现在稍作修改:将test()
设为模板功能
template <typename T>
auto test (T s)
-> decltype( char{s} );
现在test()
编译;因为类型T
的所有值都可以缩小为char
(例如T = char
)。所以test()
,模板化,本质上是错误的。
但是当你使用std::size_t
decltype( test(1UL) ) ch; // ERROR
您收到错误,因为test()
无法接受std::size_t
。这两个值都不能缩小为char
。
这正是您的代码的问题。
您的testInit()
template <typename T, typename... Args>
auto testInit(Args&&... args)
-> decltype(T{std::forward<Args>(args)...});
是可以接受的,因为有T
和Args...
类型,因此T{std::forward<Args>(args)...}
是可以接受的(例如:T = int
和Args... = int
)。
但是T = MyType
和Args... = std::size_t, char
是不可接受的,因为使用的构造函数是初始化列表为char
的构造函数,而非所有std::size_t
值可以缩小为{{ 1}}。
结论:编译char
时收到错误,因为编译decltype(testInit<MyType>(1UL, 'a')
时收到错误。
奖励回答:我建议你MyType{1000UL, 'a'}
改进(恕我直言)。
使用SFINAE和逗号运算符的强大功能,您可以编写
testInit()
所以你可以简单地写一些template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T{ args... }, std::true_type{} );
template <typename...>
std::false_type testInit (...);
static_assert()
post scriptum:如果你想要调用static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!");
static_assert( false == decltype(testInit<MyType>(1UL, 'b'))::value, "!");
构造函数,你可以使用圆括号
MyType(size_t a, char b) {}
所以,如果你用圆括号写auto y = MyType(1000UL, 'a'); // compile!
testInit()
你同时传递了template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T( args... ), std::true_type{} );
template <typename...>
std::false_type testInit (...);
s
static_assert()
答案 1 :(得分:1)
我认为@max66彻底回答了这里发生的事情; initializer_list构造函数很贪婪,因此我们必须保持警惕。
要回答第二个问题:
如何正确地检测
return {some, args};
在返回T
的函数中是否起作用,而不管它是聚合类型,用户定义的构造函数还是initializer_list构造函数?
std::is_constructible
通常是到达这里的方法,但是它仅检查括号构造是否有效,因此在您的情况下,以下static_assert
失败:
static_assert(std::is_constructible<MyType, char, char, char>::value, "");
此外,即使它 did 有效,也无法告诉我们是否需要使用花括号或常规括号来执行初始化。
因此,让别名is_constructible
为更具体的is_paren_constructible
:
template<class T, class... Args>
using is_paren_constructible = std::is_constructible<T, Args...>;
template<class T, class... Args>
constexpr bool is_paren_constructible_v =
is_paren_constructible<T, Args...>::value;
请注意,我将在此答案中使用C ++ 14和C ++ 17功能,但是我们仅使用C ++ 11就可以完成相同的工作。
现在,让我们还区分具有另一个特征is_list_constructible
的列表初始化。为此,我将利用voider模式(C ++ 17中引入了std::void_t
来辅助此操作,但我自己将其定义为更像C ++ 11):
struct voider{
using type = void;
};
template<class... T>
using void_t = typename voider<T...>::type;
template<class T, class Args, class=void>
struct is_list_constructible : std::false_type{};
template<class T, class... Args>
struct is_list_constructible<T, std::tuple<Args...>,
void_t<
decltype(T{std::declval<Args>()...})
>
>: std::true_type{};
template<class T, class... Args>
constexpr bool is_list_constructible_v =
is_list_constructible<T, std::tuple<Args...>>::value;
这使您的testInit
函数有些奇怪。我们应该使用括号构造还是列表初始化?我们总是可以将它分成两个...
template<class T, class... Args>
auto listInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});
static_assert(std::is_same<MyType, decltype(listInit<MyType>('0', 'a'))>::value, "");
template<class T, class... Args>
auto parenInit(Args&&... args) -> decltype(T(std::forward<Args>(args)...));
static_assert(std::is_same<MyType, decltype(parenInit<MyType>(1UL, 'a'))>::value, "");
但这没什么好玩的,我们宁愿有一个“做正确的事情”的入口点,所以让我们创建一个新的函数do_init
,该函数首先尝试进行列表初始化(在编译时,时间),否则将尝试括号初始化:
template<class... Args>
MyType do_init(Args&&... args)
{
constexpr bool can_list_init = is_list_constructible_v<MyType, Args...>;
constexpr bool can_paren_init = is_paren_constructible_v<MyType, Args...>;
static_assert(can_list_init || can_paren_init, "Cannot initialize MyType with the provided arguments");
if constexpr(can_list_init)
return MyType{std::forward<Args>(args)...};
else
return MyType(std::forward<Args>(args)...);
}
在main
中,我们可以调用do_init
函数,它将以适当的方式构造MyType
(否则将导致static_assert失败):
int main(){
(void)do_init('a', 'b'); // list init
(void)do_init(10000UL, 'c'); // parenthetical
(void)do_init(1UL, 'd'); // parenthetical
(void)do_init(true, false, true, false); // list init
// fails static assert
//(void)do_init("alpha");
}
我们甚至可以将is_list_constructible
和is_paren_constructible
组合成一个特征is_constructible_somehow
:
template<class T, class... Args>
constexpr bool is_constructible_somehow = std::disjunction_v<is_list_constructible<T, std::tuple<Args...>>, is_paren_constructible<T, Args...>>;
用法:
static_assert(is_constructible_somehow<MyType, size_t, char>, "");
static_assert(is_constructible_somehow<MyType, char, char, char>, "");
答案 2 :(得分:0)
如果使用std::initializer_list
初始化对象,则{}
会存在一个构造函数,这会产生一些奇怪的行为。
示例:
struct MyType
{
MyType(size_t , char ) { std::cout << "Construct via size_t/char" << std::endl;}
MyType(std::initializer_list<char> ) { std::cout << "Construct via list" << std::endl;}
};
auto x1 = MyType{1UL, 'a'};
auto x2 = MyType((1UL), 'b');
对于x1,因为您正在使用{}
语法,所以它会运行到初始化程序llist构造函数中。如果使用()
语法,则会调用 expected 构造函数。但是您遇到了最烦人的解析问题,因此需要附加的花括号!
返回代码,在您的测试功能中:
template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});
您正在使用{}
。如果更改为()
,一切正常!
template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T(std::forward<Args>(args)...));
为什么:
§13.3.1.7[over.match.list] / p1:
当非聚合类类型为
T
的对象被列表初始化时 (8.5.4),重载解析分两个阶段选择构造函数:
- 最初,候选函数是类
T
的initializer-list构造函数(8.5.4),参数列表包括 初始化程序列表作为单个参数。- 如果未找到可行的initializer-list构造函数,则会再次执行重载解析,其中所有候选函数 类
T
的构造函数和参数列表包括 初始化程序列表的元素。如果初始化列表中没有元素,并且
T
具有默认值 构造函数,省略第一阶段。在复制列表初始化中, 如果选择explicit
构造函数,则初始化为 格式不正确。
此外,构造函数列表的构造函数不允许缩小!
§8.5.4列表初始化
(3.4)否则,如果T是类类型,则考虑构造函数。适用 枚举构造函数,并通过重载选择最佳的构造函数 分辨率([over.match],[over.match.list])。如果转换范围缩小 (请参阅下文)转换任何参数, 程序格式错误。
在您的情况下,您的测试函数将捕获初始化程序列表构造函数,因为它是首选的(如果有的话),并且使用列表初始化。只是以缩小收窄失败而告终。