Richard Hodges
Richard Hodges

Reputation: 69912

Constructing a std::initializer_list of dynamic size, part II

Inspired by this question I started wondering if there was a way to create an std::initializer_list from a std::vector.

Given that c++17 guarantees RVO, it seemed to me that it might be possible by building a compile-time dispatch table of initialisation functions.

Here's the first attempt at the code to do it:

#include <initializer_list>
#include <vector>
#include <iostream>
#include <array>
#include <stdexcept>

namespace impl
{
    template<class T, std::size_t...Is>
    auto as_init_list(std::vector<T> const& v, std::index_sequence<Is...>)
    {
        auto ret = std::initializer_list<T>
        {
            v[Is]...
        };
        return ret;
    }

    template<class T, std::size_t N>
    auto as_init_list(std::vector<T> const& v)
    {
        auto ret = as_init_list(v, std::make_index_sequence<N>());
        return ret;
    }

    template<class T, std::size_t...Is>
    constexpr auto as_init_list_vtable(std::index_sequence<Is...>)
    {
        using ftype = std::initializer_list<T>(*)(std::vector<T> const&);
        auto ret = std::array<ftype, sizeof...(Is)>
        {{
            &as_init_list<T, Is>...
        }};
        return ret;
    }

    template<class T, std::size_t N>
    constexpr auto as_init_list_vtable()
    {
        auto ret = as_init_list_vtable<T>(std::make_index_sequence<N>());
        return ret;
    }
}

template<class T, std::size_t Limit = 100>
auto as_init_list(std::vector<T> const& vec)
-> std::initializer_list<T>
{
    if (vec.size() >= Limit)
        throw std::invalid_argument("too long");
    static const auto table = impl::as_init_list_vtable<T, Limit>();
    auto ret = table[vec.size()](vec);
    return ret;
}

int main()
{
    std::vector<int> v = { 1, 2, 3, 4 };
    auto i = as_init_list(v);
    for (auto&& x : i)
    {
        std::cout << x << '\n';
    }
}

Of course, as half-expected, the output appears to be UB:

4200240
32765
0
0

http://coliru.stacked-crooked.com/a/1bf92111619317dd

In this (admittedly unusual and perverse case) I seem to have transgressed some rule around the lifetime of the elements of the items in the initializer_list, but at first glance it seems to me that the code should be valid (because of guaranteed RVO).

Am I right or wrong? Does the standard cover this scenario?

Upvotes: 5

Views: 1438

Answers (2)

NathanOliver
NathanOliver

Reputation: 180955

In

auto as_init_list(std::vector<T> const& v, std::index_sequence<Is...>)
{
    auto ret = std::initializer_list<T>
    {
        v[Is]...
    };
    return ret;
}

The array you build with { v[Is]... } is a temporary object and and has it's lifetime bound to ret. Once ret goes out of scope the array is destroyed and you are left with a dangling std::initializer_list. This is covered under [dcl.init.list]/6:

The array has the same lifetime as any other temporary object ([class.temporary]), except that initializing an initializer_­list object from the array extends the lifetime of the array exactly like binding a reference to a temporary. [ Example:

typedef std::complex<double> cmplx;
std::vector<cmplx> v1 = { 1, 2, 3 };

void f() {
  std::vector<cmplx> v2{ 1, 2, 3 };
  std::initializer_list<int> i3 = { 1, 2, 3 };
}

struct A {
  std::initializer_list<int> i4;
  A() : i4{ 1, 2, 3 } {}            // ill-formed, would create a dangling reference
};

For v1 and v2, the initializer_­list object is a parameter in a function call, so the array created for { 1, 2, 3 } has full-expression lifetime. For i3, the initializer_­list object is a variable, so the array persists for the lifetime of the variable. For i4, the initializer_­list object is initialized in the constructor's ctor-initializer as if by binding a temporary array to a reference member, so the program is ill-formed ([class.base.init]). — end example ] [ Note: The implementation is free to allocate the array in read-only memory if an explicit array with the same initializer could be so allocated. — end note ]

emphasis mine

You would have to extend ret's lifetime to the point where you use it in order to not have undefined behavior.

Upvotes: 7

Vittorio Romeo
Vittorio Romeo

Reputation: 93364

The following code produces a warning on gcc (trunk):

auto foo()
{
    return std::initializer_list<int>{0, 1, 2};
}
warning: returning temporary initializer_list does not extend the lifetime of the
         underlying array [-Winit-list-lifetime]

     return std::initializer_list<int>{0, 1, 2};
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

live example on wandbox.org


This happens because std::initializer_list very roughly behaves like a pair of pointers to an array created on the stack. The lifetime of the array is not tied to the lifetime of the std::initializer_list instance.

Your as_init_list function has the same issue. Marking ret as static works around the issue: http://coliru.stacked-crooked.com/a/e0ef17af8b398fb7

Upvotes: 6

Related Questions