How to convert initializer list to another type and add an item, in compile time?

436 Views Asked by At

I wanted to figure this out, because I was running some unit tests and our system allows you to mark the test with one or more initializer lists, which will translate to multiple runs with different arguments.

So I'd have something like:

constexpr std::initializer_list<bool> bool_options{ true, false };
// some macro
DEFINE_TEST_CASE(bool_options, [](bool current_option) { ... });

In practice, this was mostly done with some enum flags. In some cases, I needed an unset option, so I did this:

template <typename T>
using opt_initializer_list = std::initializer_list<std::optional<T>>;

constexpr opt_initializer_list bool_or_unset{ true, false, std::nullopt };

Again, also happened with enums. So the question I pondered, if one test used only valid values, and other was also supposed to be run with the setting unset (did not actually happen), could I write a function to convert std::initializer_list<bool> to std::initializer_list<std::optional<bool>> and add std::nullopt?

I tried this:

#include <initializer_list>
#include <optional>

// for debug
#include <iostream>

template <typename T>
using opt_initializer_list = std::initializer_list<std::optional<T>>;

template <typename TValue>
constexpr auto make_optional_initializer_list(std::initializer_list<TValue> vals)
{
    return opt_initializer_list<TValue>{ vals, std::nullopt};
}

constexpr std::initializer_list<bool> bool_options{true, false};
// should evaluate to { true, false, std::nullopt }
constexpr opt_initializer_list<bool> bool_or_null_options = make_optional_initializer_list(bool_options);

The above will not compile, since { vals, std::nullopt} tries to invoke the begin/end iterator based constructor with invalid arguments.

But I think once you convert { ... } to an actual std::initializer_list instance, you cannot copy it in compile time. I have noticed however, that I can do this:

constexpr auto bool_options_notype = {true, false};

I am not sure what is deduced there, but if it does not count as std::initializer_list yet, but can be cast to one later, that would work - I don't need to work with initializer lists, just be able to cast the result implicitly to one.

2

There are 2 best solutions below

0
Nicol Bolas On

std::initializer_list as a type has one job: to be an intermediary tool that allows one to use a braced-init-list ({}) to initialize an object with an array. Therefore, every initializer_list will always contain the elements of a braced-init-list. You cannot manipulate them. You cannot take an initializer_list and make a new one with more or fewer elements. You cannot convert some object type into an initializer_list.

It is a runtime intermediary for a compile-time grammatical construct, and the system is designed such that the only way to make one is to use that grammatical construct. And this is a one-way conversion.

And, even if you could create one, you couldn't return such a thing. The implicit array backing the initializer_list is an automatic object, and it will be destroyed in the scope where the {} was used to create it. Thus, returning it would be returning a reference to a no-longer-existing array.

What you want is a std::array instead. You can manipulate that as you like at compile-time. Of course, you will have to use iterator-based methods to initialize a container or whatever with that array.

0
Artyer On

Your initializer list must be of the form { first_element, second_element, third_element, ... }. You can achieve this for an arbitrary number with a parameter pack made from a std::index_sequence.

The hard part is returning this from a function. Since std::initializer_list is similar to a const-reference, simply having return { true, false, std::nullopt }; would make the returned initializer_list a dangling reference. This is solved by binding it to a static variable, which lasts even after the function returns, eg:

template<typename T, const std::initializer_list<T>& IList, std::size_t... I>
constexpr opt_initializer_list<T> make_optional_initializer_list_impl(std::index_sequence<I...>) {
    // Need this intermediary lambda before C++23 as `static constexpr` is disallowed in constexpr functions
    constexpr auto lambda = []{
        static constexpr opt_initializer_list<T> result{ IList.begin()[I]..., std::nullopt };
        return std::integral_constant<const opt_initializer_list<T>*, &result>{};
    };
    return *decltype(lambda())::value;
}

template<const auto& IList>
inline constexpr auto make_optional_initializer_list = make_optional_initializer_list_impl<typename std::remove_reference_t<decltype(IList)>::value_type, IList>(std::make_index_sequence<IList.size()>{});

constexpr auto bool_or_null_options = make_optional_initializer_list<bool_options>;

static_assert(bool_or_null_options.begin()[0].value() == true);
static_assert(bool_or_null_options.begin()[1].value() == false);
static_assert(!bool_or_null_options.begin()[2].has_value());

(And passing the original initializer_list as a template argument instead, since you need to know its size at compile time).


To answer your last question, constexpr auto bool_options_notype = {true, false}; has type std::initializer_list<bool>. initializer_list objects can be copied, but often times the original initializer_list being copied from will go out of scope, making the copy refer to nothing.