Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow calling any constructor for all array elements

Tags:

c++

After some investigation, looks like the behavior initializing variables follows some convention.

For single element:

auto* x = new int; // undefined value
auto* x = new int{}; // 0 (default initializer)
auto* x = new int(23); // 23 (copy initializer)

auto* x = new Test; // default constructor
auto* x = new Test{}; // default constructor
auto* x = new Test(...); // X constructor (overload-chosen)

This makes a lot of sense, until you try to apply the same logic on arrays:

auto* x = new int[10]; // all undefined values - OK
auto* x = new int[10]{}; // all 0 (default initializer) - OK
auto* x = new int[10](23); // all 23 (copy initializer on all) - NOT IMPLEMENTED

auto* x = new Test[10]; // default constructors - OK
auto* x = new Test[10]{}; // default constructors - OK
auto* x = new Test[10](...); // X constructor on all (overload-chosen) - NOT IMPLEMENTED

My logic says that you can make some assumptions:

  • If a type can be constructed using T(Args..) then every element from the array can be constructed. There is no reason to not allow T[N](Args...) syntax.

Does anyone know why this feature doesn't exist? It would be very nice to allow

new int[10](23); // all 23
new Test[10]("asd", 123); // construct all using Test("asd", 123)

Edit: The whole idea of this is to avoid the default initialization/constructor and call the one we need directly.

like image 676
Ivan Sanz Carasa Avatar asked Feb 04 '18 17:02

Ivan Sanz Carasa


2 Answers

While it would be nice if there was something for this in the standard library, the functionality you want is relatively straightforward to implement:

namespace detail {
    template<typename T, typename... ArgTs, std::size_t... Is>
    constexpr auto make_filled_array(ArgTs const&... args, std::index_sequence<Is...>)
    -> std::array<T, sizeof...(Is)> {
        return {{(Is, T(args...))...}};
    }
}

template<typename T, std::size_t N, typename... ArgTs, std::enable_if_t<N != 0, int> = 0>
constexpr auto make_filled_array(ArgTs const&... args) {
    return detail::make_filled_array<T, ArgTs...>(args..., std::make_index_sequence<N>{});
}

// ...

auto arr1 = make_filled_array<int, 10>(23);
auto arr2 = make_filled_array<Test, 10>("asd", 123);

Online Demo

That said, I don't see any point to taking constructor arguments here; container emplace functions are useful because they do perfect-forwarding, but we can't do that here because we need to reuse the arguments for each constructor call (so moving from them is not an option). I think copy-construction is natural:

namespace detail {
    template<typename T, std::size_t... Is>
    constexpr auto make_filled_array(T const& t, std::index_sequence<Is...>)
    -> std::array<T, sizeof...(Is)> {
        return {{(Is, t)...}};
    }
}

template<std::size_t N, typename T, std::enable_if_t<N != 0, int> = 0>
constexpr auto make_filled_array(T const& t) {
    return detail::make_filled_array(t, std::make_index_sequence<N>{});
}

// ...

auto arr1 = make_filled_array<10>(23);
auto arr2 = make_filled_array<10>(Test{"asd", 123});

Online Demo

N.b. your use of new implies that you may be from a managed language and need to read up on value semantics. ;-] This code could be altered to return a std::array<T, N>* or T* (or even std::unique_ptr<T[]> for something moderately reasonable), but, why would one..? Just use std::vector<T>:

std::vector<int> vec(10, 23);

I chose to demonstrate std::array<> here because your examples all have a constant size.


The closest thing that comes to mind is std::fill, but that wouldn't initialize the array...

like image 137
ildjarn Avatar answered Oct 12 '22 23:10

ildjarn


I can't speak for the committee, but I can see a case where your syntax would be very very problematic: initializing from temporaries: .

X* x = new X[10](Y{});

How do you deal with this case?

You can't call X(Y&&) (assuming it exists) for each of the 10 elements because you have only 1 temporary and all elements after the first would be initialized from an object in an unspecified state.

The other alternative would be to somehow call X(const Y&) instead (assuming again it exists). Besides over complicating the standard and adding unexpected inconsistent behavior that just opens up another can of worms. The X object could have a reference bind to our Y{} assuming is an lvalue and you end up with a dangling reference.

The only viable solution I see is to not allow rvalue references on this syntax but that would just feed more SO questions.

I think it is pretty safe to assume the standard committee took this into consideration.

like image 38
bolov Avatar answered Oct 13 '22 00:10

bolov