Search code examples
c++list-initializationbraced-init-list

Automatic generation of a brace-enclosed initializer list in C++ using language features (NOT pre-processor directives)


I'm looking for a solution using only native C++ language features (up to C++17) for accomplishing the following:

std::array<Type, unsigned int Elem> array_{Type(), // 1    - Call constructor on Type()
                                           Type(), // 2    - ...
                                            ...  , // 3    - ...
                                           Type()} // Elems - Call the Elem:th Type() constructor

In addition, what I'd also like is that each constructor call should be able to take an arbitrary number of arguments.

A concrete example would be to automate the writing of the following:

std::array<std::shared_ptr<int>, 4> array_{std::make_shared<int>(),
                                           std::make_shared<int>(),
                                           std::make_shared<int>(),
                                           std::make_shared<int>()}

I.e., provided that I know Type and Elem, I'd like to automate the process of creating the brace-enclosed initializer list and in the process call Type:s constructor.

Any ideas?

Update, the real problem I'd like to solve is the following:

template <typename Type, unsigned int Size>
class Storage {
  public:
    Storage(std::initializer_list<Type> initializer) : array_{initializer} {}
  private:
    std::array<Type, Size> array_;
};


void foo(){
  Storage<std::shared_ptr<int>, 100> storage(...);

  // or perhaps
  auto storage = std::make_shared<Storage<std::shared_ptr<int>, 100>>(here be an initializer list containing calls to 100 std::make_shared<int>());
}


Solution

  • Like this:

    #include <array>
    #include <memory>
    #include <utility>
    
    template <std::size_t ...I>
    std::array<std::shared_ptr<int>, sizeof...(I)> foo(std::index_sequence<I...>)
    {
        return {(void(I), std::make_shared<int>())...};
    }
    
    std::array<std::shared_ptr<int>, 4> array_ = foo(std::make_index_sequence<4>());
    

    Guaranteed copy elision from C++17 ensures that the array is constructed in place, and no extra moves happen.

    The return statement expands to {(void(0), std::make_shared<int>()), (void(1), std::make_shared<int>())...}. void(...) is not strictly necessary, but Clang emits a warning otherwise.


    But if it was my code, I'd write a more generic helper instead:

    #include <utility>
    
    template <typename R, typename N, typename F, N ...I>
    [[nodiscard]] R GenerateForEach(std::integer_sequence<N, I...>, F &&func)
    {
        return {(void(I), func(std::integral_constant<N, I>{}))...};
    }
    
    template <typename R, auto N, typename F>
    [[nodiscard]] R Generate(F &&func)
    {
        return (GenerateForEach<R, decltype(N)>)(std::make_integer_sequence<decltype(N), N>{}, std::forward<F>(func));
    }
    

    Then:

    auto array_ = Generate<std::array<std::shared_ptr<int>, 4>, 4>([](auto){return std::make_shared<int>();});
    

    While the lambda discards the index in this case, it often ends up being useful, especially given that in [](auto index){...}, index.value is constexpr.