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)...));