3

Let's say I have a class without a default constructor called Foo.

If I were using an std::vector, I could do this:

std::vector<Foo> vec(100, Foo(5));

This would create a vector of 100 elements, each with value Foo(5).

How do I do the same with std::array<Foo, 100>?

I obviously do not want to list out Foo(5) explicitly 100 times in an initializer list. And yet I cannot wait till after the array is constructed to initialize it, since the lack of default constructor will produce a compiler error.

Bonus points for a solution that allows me to avoid the copy constructor as well, by supplying explicit constructor arguments similar to "placement new" or emplace functions.

Matt
  • 21,026
  • 18
  • 63
  • 115
  • Your vector example does use copy constructor. It seems a bit unfair to hold `std::array` to a higher standard. – Igor Tandetnik Feb 07 '22 at 05:14
  • There's nothing "unfair" about it. I just want to find out how to do this. If I were actually using a vector, I could avoid the copy constructor in other ways. But that is optional anyway. – Matt Feb 07 '22 at 05:22
  • I figured out how to do it without copying, after all. See my updated answer. – Igor Tandetnik Feb 07 '22 at 15:24

2 Answers2

6

With copy constructor, something along these lines:

template <typename T, size_t... Is>
std::array<T, sizeof...(Is)> MakeArrayHelper(
    const T& val, std::index_sequence<Is...>) {
  return {(static_cast<void>(Is), val) ...};
}

template <typename T, size_t N>
std::array<T, N> MakeArray(const T& val) {
  return MakeArrayHelper<T>(val, std::make_index_sequence<N>{});
}

std::array<Foo, 100> arr = MakeArray<Foo, 100>(Foo(5));

Actually, this can be done without copy constructor after all. This solution relies heavily on C++17's mandatory copy elision.

template <typename T, size_t... Is, typename... Args>
std::array<T, sizeof...(Is)> MakeArrayHelper(
    std::index_sequence<Is...>, Args&&... args) {
  return {(static_cast<void>(Is), T{std::forward<Args>(args)...}) ...};
}

template <typename T, size_t N, typename... Args>
std::array<T, N> MakeArray(Args&&... args) {
  return MakeArrayHelper<T>(std::make_index_sequence<N>{},
                            std::forward<Args>(args)...);
}

Demo

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • Thanks. It's ugly, but it could do for now. And probably better than a preprocessor macro. – Matt Feb 07 '22 at 05:31
  • I think this `(Is, val), ...` should be `(Is, val)...`. And `std::array arr...` should be `auto arr = MakeArray(Foo(5))`. – zdf Feb 07 '22 at 11:03
  • @zdf Fixed the typo, the comma indeed shouldn't be there. `Foo` is meant to be a type, not a function, so `foo_return_type` doesn't make sense. – Igor Tandetnik Feb 07 '22 at 15:23
  • 1
    @Matt I figured out how to do it without copying. Updated the answer. – Igor Tandetnik Feb 07 '22 at 15:24
  • `(Is, val)` -> `(static_cast(Is), val)` to handle evil overloaded operator comma. – Jarod42 Feb 07 '22 at 16:02
  • @Jarod42 This gets rid of the warning, too, so I can drop that `JustT` thing. Updated answer. – Igor Tandetnik Feb 07 '22 at 16:09
  • Thanks for the updates!! I hope in the future `std::array` will improve its initialization support so that things like this can be avoided. – Matt Feb 07 '22 at 18:28
  • @Matt The problem is, `std::array` can't have any user-defined constructors, as it's required by the standard to be an aggregate. So there's little space for improvement. – Igor Tandetnik Feb 07 '22 at 20:31
  • That's not necessarily an obstacle. It's not inconceivable that more support for such initialization could be added to the language spec. Perhaps something like `= { x ... }` automatically expanding to an unlimited number of `x`'s. – Matt Feb 08 '22 at 16:03
2

std::array is just a thin wrapper around a fixed array. It has no repeat-insert logic, like std::vector does. Since Foo does not have a default constructor, the only way to initialize an instance of std::array<Foo, N> is to use aggregate initialization with N number of values specified (sorry, I know you don't want to to do this), eg:

std::array<Foo, 100> arr{5, 5, 5, ...}; // N times...

Otherwise, you will have to create a byte array of sufficient size and then use placement-new in a loop, eg:

std::aligned_storage_t<sizeof(Foo), alignof(Foo)> arr[100];
for(int i = 0; i < 100; ++i) {
   new (&arr[i]) Foo(5);
}
...
// use static_cast<Foo*>(&arr[index]) to access each object as needed...
...
for(int i = 0; i < 100; ++i) {
    // when using placement-new, you must call each object's destructor explicitly...
    static_cast<Foo*>(&arr[i])->~Foo();
}

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Is there some alternative to `std::array` that could do this? The manual byte array would work, but it wouldn't provide the nice vector-like interface with all modern C++ goodies. I could roll my own of course, but I was hoping it wouldn't be necessary. – Matt Feb 07 '22 at 05:30
  • placement new way is very dangerous, as copy/move has also to be handled... :/ – Jarod42 Feb 07 '22 at 16:05
  • 1
    Danger is in the eyes of the beholder. People who are used to programming in C and old school C++ would have nothing to fear. Yet, it is yet another case of "why do I have to roll my own?" – Matt Feb 07 '22 at 18:30
  • Minor detail, aligned_storage and crew it's recommended to do it "more manually" with alignas and byte arrays ( https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1413r3.pdf ) . I am facing a similar issue and working on an "uninitialized array" in my own `estdlib` for embedded – Malachi Dec 28 '22 at 00:27