Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deducing type of initializer list by first element

Is the following code correct according to the Standard (note that type deduced by first element in the list is applied to the second list-initialized 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)>, "!");
}

Recently, I wanted to utilize std::initializer_list as continuous storage for Vulkan structures:

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
    }
};

And found out that the above syntax works. The syntax is more terse then writing std::initializer_list<vk::SubpassDependency> instead of auto, but compiler is GCC (set(CMAKE_CXX_STANDARD 20)) and I don't know is it GCC specific extension/oversight or standard core language feature.

Previously I thought I have to specify types of all the elements during list initialization in explicit manner to get valid initializer for std::initializer_list via auto x = {<list>}; syntax.

like image 953
Tomilov Anatoliy Avatar asked Nov 07 '25 03:11

Tomilov Anatoliy


2 Answers

This is standard to my reading. The key here is in the deduction process

[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,

Each element of the initializer list is taken as an argument to deduce the element type from. For the first one, deduction succeeds. Whereas the others are non-deduced contexts

[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 ]

For template argument deduction to succeed, a template parameter must agree in its deduction from different arguments, so long as deduction is possible from an argument. If there is a consensus (even of just one) deduction succeeds.

[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.

By virtue of being non-deduced contexts, the trailing elements can't disagree with the deduction done from the first element. So template argument deduction should be successful. Then, with the type correctly deduced, initialization can proceed for each element of the std::initializer_list from its corresponding initializer.

like image 99
StoryTeller - Unslander Monica Avatar answered Nov 08 '25 17:11

StoryTeller - Unslander Monica


I think that, yes, this code is "correct" in the sense that this is supposed to compile and dependencies is guaranteed to be an std::initializer_list<vk::SubpassDependency>. The type of dependencies will be deduced from your top-level braced-init-list via template argument deduction for a parameter of type std::initializer_list<U> where U is an invented template parameter [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]/1. There are no other elements of that braced-init-list from which a conflicting deduction could be made. Thus, the type of dependencies will be deduced to std::initializer_list<vk::SubpassDependency>.

Initializing elements of type vk::SubpassDependency from the remaining braced-init-lists during construction of the initializer list object does not involve a narrowing conversion [dcl.init.list]/7, so this should be well-formed.

Despite this being well-formed, I woud argue that readability of this approach is not great. Since no explicit type is given for dependencies and only one lone element of the braced-init-list has a type at all, one is almost forced to wonder whether this actually does what one thinks it probably does. If code that's supposed to do something as simple as initializing an array of things requires consulting a language lawyer to figure out whether that's really actually what it does, then that code should probably not be written that way. Why not simply write

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
    }
};

This essentially does the exact same thing [dcl.init.list]/5, just that everyone should immediately understand what's going on here…

like image 38
Michael Kenzel Avatar answered Nov 08 '25 17:11

Michael Kenzel