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

时间:2017-11-17 04:09:15

标签: c++ c++11 templates variadic-templates narrowing

背景:我正在编写一个类似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...

我认为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}构造函数?

3 个答案:

答案 0 :(得分:1)

这有点复杂。

而且我并不是真正的专家,所以我可以说一些不完全准确的话:用一点点盐来说我说的话。

首先:写作时

auto x = MyType{1UL, 'a'};  // ok

调用的构造函数是初始化列表,而不是接收std::size_tchar的构造函数。

这是有效的,因为第一个值1ULunsigned 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;但如果传递1ULtest()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)...});

是可以接受的,因为有TArgs...类型,因此T{std::forward<Args>(args)...}是可以接受的(例如:T = intArgs... = int)。

但是T = MyTypeArgs... = 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_constructibleis_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>, "");

Demo

答案 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])。如果转换范围缩小 (请参阅下文)转换任何参数, 程序格式错误。

在您的情况下,您的测试函数将捕获初始化程序列表构造函数,因为它是首选的(如果有的话),并且使用列表初始化。只是以缩小收窄失败而告终。