通过第一个元素推导初始化列表的类型

Deducing type of initializer list by first element

根据标准,以下代码是否正确(请注意,列表中第一个元素推导的类型应用于第二个列表初始化元素)?

#include <type_traits>
#include <initializer_list>

struct A {};

int main()
{
    auto l = {A{}, {}};
    static_assert(std::is_same_v<std::initializer_list<A>, decltype(l)>, "!");
}

最近,我想利用 std::initializer_list 作为 Vulkan 结构的连续存储:

auto dependencies = {
    vk::SubpassDependency{
        VK_SUBPASS_EXTERNAL,
        0,
        vk::PipelineStageFlagBits::eBottomOfPipe,
        vk::PipelineStageFlagBits::eColorAttachmentOutput,
        vk::AccessFlagBits::eMemoryRead,
        vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite,
        vk::DependencyFlagBits::eByRegion
    },
    {
        0,
        VK_SUBPASS_EXTERNAL,
        vk::PipelineStageFlagBits::eColorAttachmentOutput,
        vk::PipelineStageFlagBits::eBottomOfPipe,
        vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite,
        vk::AccessFlagBits::eMemoryRead,
        vk::DependencyFlagBits::eByRegion
    }
};

并发现上述语法有效。语法比 std::initializer_list<vk::SubpassDependency> 而不是 auto 更简洁,但编译器是 GCC (set(CMAKE_CXX_STANDARD 20)) 我不知道它是不是 GCC特定 extension/oversight 或标准核心语言功能。

以前我认为我必须在列表初始化期间以显式方式指定所有元素的类型,以便通过 auto x = {<list>}; 语法获得 std::initializer_list 的有效初始化程序。

这是我阅读的标准。这里的关键在于推导过程

[temp.deduct.call]

1 If removing references and cv-qualifiers from P gives std​::​initializer_­list<P'> or P'[N] for some P' and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument,

初始化列表中的每个元素都作为参数来推断元素类型。对于第一个,推演成功。而其他的是非推导上下文

[temp.deduct.type]

5 The non-deduced contexts are:

  • A function parameter for which the associated argument is an initializer list ([dcl.init.list]) but the parameter does not have a type for which deduction from an initializer list is specified ([temp.deduct.call]). [ Example:

    template<class T> void g(T);
    g({1,2,3});                 // error: no argument deduced for T
    

     — end example ]

要使模板参数推导成功,模板参数必须同意从不同参数推导,只要可以从一个参数推导即可。如果达成共识(即使只有一个),推论成功。

[temp.deduct.type]

2 In some cases, the deduction is done using a single set of types P and A, in other cases, there will be a set of corresponding types P and A. Type deduction is done independently for each P/A pair, and the deduced template argument values are then combined. If type deduction cannot be done for any P/A pair, or if for any pair the deduction leads to more than one possible set of deduced values, or if different pairs yield different deduced values, or if any template argument remains neither deduced nor explicitly specified, template argument deduction fails. The type of a type parameter is only deduced from an array bound if it is not otherwise deduced.

由于是非推导上下文,尾随元素不能与从第一个元素进行的推导不一致。所以模板参数推导应该是成功的。然后,在正确推导出类型的情况下,可以从相应的初始化程序对 std::initializer_list 的每个元素进行初始化。

我认为,是的,这段代码是 "correct",因为它应该编译并且 dependencies 保证是 std::initializer_list<vk::SubpassDependency>dependencies 的类型将从您的顶级 braced-init-list 通过模板参数推导推导出类型为 std::initializer_list<U> 的参数,其中 U 是一个发明的模板参数 [dcl.type.auto.deduct]/4. Your top-level braced-init-list has one element from which U can be deduced to vk::SubpassDependency [temp.deduct.call]/1braced-init-list 中没有其他元素可以从中得出相互矛盾的推论。因此,dependencies 的类型将被推断为 std::initializer_list<vk::SubpassDependency>.

在初始化列表对象的构造过程中,从剩余的 braced-init-list 中初始化 vk::SubpassDependency 类型的元素不涉及缩小转换 [dcl.init.list]/7, 所以这应该是合式的。

尽管格式正确,但我认为这种方法的可读性并不好。由于没有为 dependencies 给出显式类型,并且 braced-init-list 中只有一个元素有类型,所以人们几乎不得不怀疑这是否确实如此人们认为它可能会做什么。如果代码应该做一些像初始化数组这样简单的事情需要咨询语言律师来弄清楚它是否真的是它所做的,那么代码可能不应该那样写。为什么不简单地写

vk::SubpassDependency dependencies[] = {
    {
        VK_SUBPASS_EXTERNAL,
        0,
        vk::PipelineStageFlagBits::eBottomOfPipe,
        vk::PipelineStageFlagBits::eColorAttachmentOutput,
        vk::AccessFlagBits::eMemoryRead,
        vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite,
        vk::DependencyFlagBits::eByRegion
    },
    {
        0,
        VK_SUBPASS_EXTERNAL,
        vk::PipelineStageFlagBits::eColorAttachmentOutput,
        vk::PipelineStageFlagBits::eBottomOfPipe,
        vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite,
        vk::AccessFlagBits::eMemoryRead,
        vk::DependencyFlagBits::eByRegion
    }
};

这基本上做了完全相同的事情[dcl.init.list]/5,只是每个人都应该立即明白这里发生了什么……

来自 C++{ 17 标准(8.2.3 显式类型转换(函数符号)

1 A simple-type-specifier (10.1.7.2) or typename-specifier (17.6) followed by a parenthesized optional expression list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer. If the type is a placeholder for a deduced class type, it is replaced by the return type of the function selected by overload resolution for class template deduction (16.3.1.8) for the remainder of this section.

所以这个

vk::SubpassDependency{
    VK_SUBPASS_EXTERNAL,
    0,
    vk::PipelineStageFlagBits::eBottomOfPipe,
    vk::PipelineStageFlagBits::eColorAttachmentOutput,
    vk::AccessFlagBits::eMemoryRead,
    vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite,
    vk::DependencyFlagBits::eByRegion
}

是一个带有显式转换的表达式。