5

背景:我正在编写一个类似的包装器类型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};会在返回的函数中工作Tinitializer_list

4

3 回答 3

3

这有点复杂。

而且我不是真正的专家,所以我可以说一些不完全准确的东西:拿我说的话加一点盐。

首先:当你写

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

调用的构造函数是初始化列表之一,而不是接收 astd::size_tchar.

这是有效的,因为第一个值1UL是 an但具有可以缩小到unsigned long int的值(注意:achar) 。那就是: 有效,因为1UL它是一个适合 a 的值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} );

此函数立即给出编译错误。

你可以这样想:“但是如果传递1ULtest()decltype()可以将值缩小std::size_t到一个char

问题是 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. 既不是一个可以缩小到 a 的值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 = MyTypeandArgs... = std::size_t, char是不可接受的,因为使用的构造函数是具有初始化列表的构造函数,char并且并非所有std::size_t值都可以缩小到 a char

结论:你得到一个错误编译decltype(testInit<MyType>(1UL, 'a'),因为你得到一个错误编译MyType{1000UL, 'a'}

奖励答案:我建议对您的testInit().

使用 SFINAE 和逗号运算符的强大功能,您可以编写

template <typename T, typename... Args>
auto testInit (Args ... args)
   -> decltype( T{ args... }, std::true_type{} );

template <typename...>
std::false_type testInit (...);

所以你可以static_assert()简单地写一些如下

static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!"); 
static_assert( false == decltype(testInit<MyType>(1UL, 'b'))::value, "!"); 

Post scriptum:如果要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 (...);

你通过了以下static_assert()两个

static_assert( true == decltype(testInit<MyType>('a', 'b'))::value, "!"); 
static_assert( true == decltype(testInit<MyType>(1UL, 'b'))::value, "!"); 
于 2017-11-17T14:19:59.410 回答
1

我认为@max66彻底回答了这里发生的事情;initializer_list 构造函数是贪婪的,所以我们必须小心。

回答你的第二个问题:

无论它是聚合类型、用户定义的构造函数还是 initializer_list 构造函数,我如何才能正确检测是否return {some, args};会在返回的函数中工作?T

std::is_constructible通常去这里的方式,但是它只检查括号构造是否有效,所以在你的情况下,以下static_assert失败:

static_assert(std::is_constructible<MyType, char, char, char>::value, "");

此外,即使它确实有效,也无法判断我们是否需要使用花括号或常规括号来执行初始化。

所以,让我们别名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-pattern(std::void_t在 C++17 中引入以帮助实现这一点,但我自己将其定义为更像 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>, "");

演示

于 2020-02-11T15:19:53.260 回答
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 构造函数中,因为您使用的是{}语法。如果使用该()语法,则会调用预期的构造函数。但是你遇到了最棘手的解析问题,所以需要额外的大括号!

回到你的代码,在你的测试函数中:

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)时,重载决议分两个阶段选择构造函数:

  • 最初,候选函数是类的初始化列表构造函数(8.5.4),T参数列表由初始化列表作为单个参数组成。
  • 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数是类的所有构造函数,T参数列表由初始化列表的元素组成。

如果初始化列表没有元素并且T有默认构造函数,则省略第一阶段。在复制列表初始化中,如果explicit选择了构造函数,则初始化格式错误。

此外,初始化列表构造函数不允许缩小!

§8.5.4 列表初始化

(3.4) 否则,如果 T 是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议([over.match]、[over.match.list])选择最佳构造函数。如果需要缩小转换(见下文)来转换任何参数,则程序格式错误。

在您的情况下,您的测试函数会捕获初始化程序列表构造函数,因为它是可用的,并且使用了列表初始化。只是以缩小失败告终。

于 2020-02-11T16:10:37.300 回答