1

Suppose the struct is supplied with two template arguments, one being typename T and the other one being size_t N. Now, that struct should store a static array of element type T and size N. In some cases, it might be fine to initialize the array with default values upon creation of an instance, which is what I think happens anyway if you have something like

template<typename T, size_t N>
struct Foo {
    T values[N];
    size_t size;
    
    explicit Foo(size_t _size) : size{ _size } {}
}

So, as said, I think the behaviour in this case is that values just automatically gets initialized with default values (calling the default constructor of T). But what if I want to pass some values for the array when constructing an object? The goal here is that I am able to pass a static array to the constructor and let that static array take the place of values. And ideally, there would be only two array creations in the whole procedure. But is that even possible? Consider the following example:

template<typename T, size_t N>
struct Foo {
    T values[N];
    size_t size;
    
    explicit Foo(T _values[N], size_t _size) : values{ _values }, size{ _size} {}
}

Now, apart from me not even knowing if the above would work as expected, I am still a bit unsure about when copies happen in C++. I'd imagine that in the worst case, there would be 4 arrays created when calling the constructor of Foo:

  1. To call the constructor of Foo, you need to pass an array, so you need to create that beforehand.
  2. The constructor parameter _values is pass-by-value, so a copy is happening here.
  3. Before the constructor initializes any values, the static array is already initialized (?)
  4. When assigning _values to values, another copy is happening (?)

Now as said I'm really not sure about the copying behaviours in C++. Of course, we can rule out one array creation by making the _values parameter pass-by-reference. But still, the static array values would be initialized before being overwritten by _values, or so I think.

My question, therefore, is: What is the best strategy here? How do I have to write the code so that I trigger the least amount of array creations?

Thanks!

Edit:

No, I cannot use std::vector or any other data structure from the stdlib. And even if I could, my question is still about the best strategy for raw arrays, and not how to switch out my approach in favour of some wrapper.

Raphael Tarita
  • 391
  • 2
  • 18
  • 1
    Unless you're tasked with writing your own array wrapper class, just use `std::vector` and avoid all of this mess. – tadman May 13 '21 at 21:53
  • 2
    "struct should store a static array". *Either* the struct should store the array, *or* the array should be static, both together make no sense. – n. 'pronouns' m. May 13 '21 at 21:54
  • 2
    Don't forget arrays as arguments are [mostly lies](https://stackoverflow.com/questions/1461432/what-is-array-to-pointer-decay). – tadman May 13 '21 at 21:55
  • 1
    @tadman Or even [`std::array`](https://en.cppreference.com/w/cpp/container/array), which looks like a much better fit for this definition. – cdhowie May 13 '21 at 21:58
  • @cdhowie Depends on the size involved. `array` can be pretty punishing on the stack. I'm just saying use `std::vector` as a default. `std::array` if conditions permit. – tadman May 13 '21 at 21:58
  • @tadman 1. Well, I'm not exactly tasked to write a generic array wrapper, but I do have to implement a data structure without the use of `std::vector`, or actually without most of the stdlib. 2. About the array to pointer decay: As far as I understood it, it does not happen to static arrays. Is that right? – Raphael Tarita May 13 '21 at 22:00
  • @n.'pronouns'm. I was referring to arrays with a size that is known at compile time, or in other words when you have to specify the size at declaration. This is not about the `static` modifier. If I'm using the wrong terminology here, please correct me. – Raphael Tarita May 13 '21 at 22:02
  • @HolyBlackCat That sounds like a solution to me, thank you!. However, I am not very proficient in C++, could you refer me to some resources about aggregate initialization and/or placement-new? You can of course also post that information as a solution. – Raphael Tarita May 13 '21 at 22:07
  • It should be possible to make this work using `std::array` and `std::initializer_list`. Do you know how to use these templates from the C++ library? – Sam Varshavchik May 13 '21 at 22:08
  • @SamVarshavchik please refer to my latest edit; I cannot use `std::array` and the usage of `std::initializer_list` is also questionable in the scope of the task. – Raphael Tarita May 13 '21 at 22:09
  • 1
    @tadman `std::array` doesn't have to be used on the stack either, it could be used as part of a larger heap allocation. `std::vector` has the downside of additional indirection and heap allocation overhead regardless of the storage duration of the vector object itself. I wouldn't go so far as to say to prefer one or the other, I'd say use the one that makes sense in a given situation. – cdhowie May 13 '21 at 22:09
  • 1
    If you cannot use most tools and classes from the C++ library, there's very little that you can do all by yourself. – Sam Varshavchik May 13 '21 at 22:10
  • @SamVarshavchik so you'd say that the strategy with a pass-by-reference `_values` might already be the best strategy? I mean that's also an answer for me – Raphael Tarita May 13 '21 at 22:16
  • In C++ arrays cannot be passed, by reference of value. That's why I mentioned std::array and std::initializer_list. – Sam Varshavchik May 13 '21 at 23:13
  • Yeah, "fixed-size array" would have been better, but just "array" would work just as well. There are no other kinds of arrays that a class object can store. – n. 'pronouns' m. May 14 '21 at 08:37

2 Answers2

0

To avoid the copy and the pointer decay, _values can be passed by reference:

template<typename T, std::size_t N>
struct Foo {
    T values[N];
    std::size_t size;
    
    explicit Foo(T const(&_values)[N], std::size_t _size) : size{ _size}{
         std::copy(std::begin(_values), std::end(_values), std::begin(values));
    }
};

https://godbolt.org/z/dnb9zTooG

but better use std::array<T, N> values; member.


This answer created some controversy. To be clear, if the problem is the initialization of the nested array, you are better off without (any) constructor and use aggregate initialization. Which is very limiting if you want do do other things with the class. At the end you are reinventing std::array anyway.

alfC
  • 10,293
  • 4
  • 42
  • 88
  • 1
    Will this not default-initialize the members of `Foo::values` before the `std::copy()` call? – cdhowie May 13 '21 at 22:11
  • As far as I understand it, there is still a lot of array creation happening. From my knowledge, `_values` is passed by value and `values` is still initialized before the `std::copy()` call (as @cdhowie pointed out). Also, I have to look up the task specification again, but I fear I will not be allowed to use `std::copy()`. – Raphael Tarita May 13 '21 at 22:14
  • @cdhowie, I think they will be uninitialized to start with. In any case the OP should use `std::array`. – alfC May 13 '21 at 22:21
  • @RaphaelTarita, if that is the problem, don't pass by value, pass by reference. if you can't use `std::copy` roll your own loop. – alfC May 13 '21 at 22:23
  • @NathanOliver is that the case? I am unsure now whether this also happens to static arrays. – Raphael Tarita May 13 '21 at 22:23
  • You still have the issue with the default initialization of `_values`. If `T` has a default constructor, it will default construct each array member. – NathanOliver May 13 '21 at 22:24
  • Old comment removed since passing by reference doesn't cause the array to decay. – NathanOliver May 13 '21 at 22:24
  • @alfC I cannot use std::array. Refer to the edit of my question. – Raphael Tarita May 13 '21 at 22:25
  • @NathanOliver, there no way around it, if you wan't to avoid default construction, you have to use an unitialized char-like buffer. – alfC May 13 '21 at 22:26
  • Sure there is, see how `std::array` works. It uses aggregate initialization so the array gets initialized like a regular one would. – NathanOliver May 13 '21 at 22:27
  • 1
    @RaphaelTarita, if you can't use `std::array` you will end up reinventing it indirectly. It is not too hard either, maybe it is a 20 line class at most. – alfC May 13 '21 at 22:27
  • @NathanOliver, do you mean `T values[N] = {};`? In any case, this is my point, use `std::array`. – alfC May 13 '21 at 22:28
  • The OP can't. There assignment is literally create your own version of `std::array` – NathanOliver May 13 '21 at 22:30
  • @NathanOliver, ah, I see what you mean, for that he doesn't need a constructor in the first place. Remove the constructor and just say `Foo f{ {1, 2, 3, 4, 5}, 42};`. – alfC May 13 '21 at 22:33
  • @NathanOliver, if this is a homework assigment, the OP just copy an implementation from the web https://www.boost.org/doc/libs/1_76_0/doc/html/boost/array.html. What is the difference with asking in SO? I shouldn't be answering homework problems in SO. – alfC May 13 '21 at 22:34
  • 1
    They asked because they didn't know that's what they needed. – NathanOliver May 13 '21 at 22:41
0

You can do it like this:

#include <cstddef>
#include <type_traits>
#include <utility>
#include <algorithm>

using namespace std;

struct S
{
    S();
    S(int);
    S(S const&);
    S(S&&);
    ~S();
    S& operator=(S const&);
    S& operator=(S&&);
};


template<class F>
struct inplacer
{
    F f_;
    operator std::invoke_result_t<F&>() { return f_(); }
};

template<class F> inplacer(F) -> inplacer<F>;


template<size_t maxN, typename T>
struct myarray
{
    size_t      sz_{};
    T           data_[maxN];
    
    template<class... Ts>
    myarray(Ts&&... args) : sz_{sizeof...(Ts)}, data_{forward<Ts>(args)...} {}

    template<class E, size_t N>
    static myarray make(E const (&a)[N], size_t count = N)
    {
        return make_copy(a, count, make_index_sequence<maxN>());
    }

    template<class E, size_t N>
    static myarray make(E (&&a)[N], size_t count = N)
    {
        return make_move(a, count, make_index_sequence<maxN>());
    }

private:
    template<class E, size_t... Indices>
    static myarray make_copy(E const* p, size_t count, index_sequence<Indices...>)
    {
        auto r = myarray( inplacer{ [&]{ return Indices < count ? p[Indices] : T(); } }... );
        r.sz_ = min(count, maxN);
        return r;
    }

    template<class E, size_t... Indices>
    static myarray make_move(E* p, size_t count, index_sequence<Indices...>)
    {
        auto r = myarray( inplacer{ [&]{ return Indices < count ? move(p[Indices]) : T(); } }... );
        r.sz_ = min(count, maxN);
        return r;
    }
};


auto f()
{
    auto v = myarray<16, int>(1, 2);

    //myarray<100, int> v = {3, 4};

    //static int d[] = {1, 2, 3, 4};
    //auto v = myarray<12, int>::make(d, 3);

    //myarray<4, S> v = {S(1), 2};
    //myarray<4, S> v = {inplacer{ []{ return S(1); } }, 2};

    //static S d[] = {3, 4, 1};
    //auto v = myarray<4, S>::make(d, 2);
    //auto v = myarray<4, S>::make(move(d));

    return v;
}

I bet this can be improved in many ways, I am going to leave it up to you...

Notes:

  • no matter how hard I tried -- I couldn't get GCC to default-initialize unused elements of array. I tried to populate them with result of trick<T>() call, but it didn't work... It complained about unused variable and insisted on filling the rest of array with zeroes.

    template<class T> T trick() { T r; return r; } 
    
  • which means this class is quite inefficient, better approach would be to use raw storage and manually control it (maybe try putting array into an unrestricted union? be warned though -- compilers have bugs when it comes to handling unions)

  • also keep in mind that compilers don't like too many function parameters -- related optimizations work nicely only for relatively small maxN values. Code generated for myarray<1000, int>::make(d, 3) looks horrendous and compilation time shoots into stratosphere...

  • myarray ctor allows for aggregate-init-like initialization, it is pretty close but not entirely same. It doesn't permit in-place construction (simply because arguments need to be constructed before call is made) -- you can use inplacer to overcome this (see one of examples in f())

C.M.
  • 2,353
  • 1
  • 11
  • 25