C++ 如何检测类型是否可初始化列表?
背景:我正在编写一个包装器类型,比如C++ 如何检测类型是否可初始化列表?,c++,c++11,templates,variadic-templates,narrowing,C++,C++11,Templates,Variadic Templates,Narrowing,背景:我正在编写一个包装器类型,比如other,我希望返回{some,args}从返回的函数开始工作与从返回a或B的函数开始工作的时间相同。但是,我还想检测何时可以使用{some,args}初始化A和B,并产生错误以保护用户不受歧义的影响 为了检测类型T是否可以从一些参数初始化,我尝试编写如下函数: template<typename T, typename... Args> auto testInit(Args&&... args) -> decltype(T
other
,我希望返回{some,args}
从返回的函数开始工作
与从返回a
或B
的函数开始工作的时间相同。但是,我还想检测何时可以使用{some,args}
初始化A
和B
,并产生错误以保护用户不受歧义的影响
为了检测类型T
是否可以从一些参数初始化,我尝试编写如下函数:
template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});
// imagine some other fallback overload here...
但是,当我们从std::initializer\u list
添加构造函数时,它会中断:
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, "");
struct MyType{
MyType(大小为a,字符b){}
MyType(std::初始值设定项_list x){}//新建!
};
自动x=MyType{1UL,'a'};//还可以吗
//失败:
静态断言(std::is_same::value,“”);
注意:忽略候选模板:替换失败[使用T=MyType,Args=]:在初始值设定项列表中,非常量表达式不能从类型“unsigned long”缩小为“char”
auto testInit(Args&&... args) -> decltype(T{std::forward<Args>(args)...});
^ ~~~
autotestinit(Args&&…Args)->decltype(T{std::forward(Args)…});
^ ~~~
为什么Clang拒绝解析我的
(size\u t,char)
构造函数而支持初始值设定项列表
构造函数如何正确检测是否返回{some,args}
将在返回T
的函数中工作,而不管它是聚合类型、用户定义的构造函数还是初始值设定项\u list
构造函数?这有点复杂
我不是一个真正的专家,所以我可以说一些不完全准确的话:对我说的话稍加保留
首先:当你写作的时候
auto x = MyType{1UL, 'a'}; // ok
调用的构造函数是初始值设定项列表,而不是接收std::size\t
和char
的构造函数
这是因为第一个值,1UL
是一个无符号长int
,但其值(注意:a值)可以缩小到char
。也就是说:之所以有效,是因为1UL
是一个适合char
的值
如果你尝试
auto y = MyType{1000UL, 'a'}; // ERROR!
您得到一个错误,因为1000UL
不能缩小为char
。也就是说:1000UL
不适合char
这也适用于decltype()
:
但请考虑这个函数
auto test (std::size_t s)
-> decltype( char{s} );
此函数立即给出编译错误
您可以这样想:“但是如果通过1UL
到test()
,decltype()
可以将std::size\t
值缩小到char
”
问题是C和C++是强类型语言;如果您允许test()
工作,并返回一个类型,那么当接收到一些std::size\t
的值时,您可以(通过SFINAE)创建一个函数,该函数返回某些值的类型和另一个具有其他类型的类型。从强类型语言的角度来看,这是不可接受的
所以
仅当decltype(char{s})
对于s
的所有可能值都是可接受的时,才可接受。也就是说:test()
是不可接受的,因为std::size\u t
可以容纳1000UL
而不适合char
现在有一点变化:使test()
成为一个模板函数
template <typename T>
auto test (T s)
-> decltype( char{s} );
出现错误是因为test()
无法接受std::size\t
。两者都不是可以缩小为char
的值
这正是代码的问题所在
您的testInit()
因此,您可以编写一些静态断言()
,如下所示
static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!");
static_assert( false == decltype(testInit<MyType>(1UL, 'b'))::value, "!");
因此,如果您使用圆括号编写testInit()
template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T( args... ), std::true_type{} );
template <typename...>
std::false_type testInit (...);
我认为这完全回答了这里发生的事情;初始值设定项列表构造函数是贪婪的,所以我们必须小心
回答你的第二个问题:
如何正确检测是否返回{some,args}
将在返回T
的函数中工作,而不管它是聚合类型、用户定义的构造函数还是初始值设定项\u列表构造函数
通常是这样,但是它只检查插入式构造是否有效,因此在您的情况下,以下static\u assert
失败:
static_assert(std::is_constructible<MyType, char, char, char>::value, "");
注意,在这个答案中我将使用C++14和C++17特性,但是我们可以用C++11完成同样的事情
现在,让我们也用另一个特性来区分列表初始化,是可构造的。为此,我将使用voider模式(std::void\t
是在C++17中引入的,以帮助实现这一点,但我自己将其定义为更像C++11):
但这并不有趣,我们更希望有一个“只做正确的事情”的入口点,因此让我们创建一个新函数,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)...);
}
我们甚至可以将是可构造的
和是可构造的
组合成一个单独的特征,是可构造的
:
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...>>;
模板
constexpr bool是可构造的,如果使用std::initializer\u 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');
您正在使用{}
。如果更改为()
,一切正常
template<typename T, typename... Args>
auto testInit(Args&&... args) -> decltype(T(std::forward<Args>(args)...));
模板
自动测试(Args&&…Args)->decltype(T(std::forward(Args)…);
原因:
§13.3.1.7[超过匹配列表]/p1:
当初始化非聚合类类型T
的对象时
(8.5.4),过载分辨率分两个阶段选择构造函数:
- 最初,候选函数
auto y = MyType(1000UL, 'a'); // compile!
template <typename T, typename... Args>
auto testInit (Args ... args)
-> decltype( T( args... ), std::true_type{} );
template <typename...>
std::false_type testInit (...);
static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!");
static_assert( true == decltype(testInit<MyType>(1UL, 'b'))::value, "!");
static_assert(std::is_constructible<MyType, char, char, char>::value, "");
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;
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;
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, "");
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)...);
}
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");
}
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>, "");
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');
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)...));