3

In C++11, is there a DRY way to construct all elements of an array with some same set of parameters for all elements? (e.g. via a single initializer list?)

For example:

class C {
public:
   C() : C(0) {}
   C(int x) : m_x{x} {}
   int m_x;
};

// This would construct just the first object with a parameter of 1.
// For the second and third object the default ctor will be called.
C ar[3] {1};

// This would work but isn't DRY (in case I know I want all the elements in the array to be initialized with the same value.
C ar2[3] {1, 1, 1};

// This is DRYer but obviously still has repetition.
const int initVal = 1;
C ar3[3] {initVal, initVal, initVal};

I know my goal is easily achievable by using an std::vector. I'm wondering if it's possible with raw arrays as well.

Danra
  • 9,546
  • 5
  • 59
  • 117

4 Answers4

4

c++14 - a little work will make this work for c++11

#include <iostream>
#include <array>
#include <utility>

class C {
public:
    C() : C(0) {}
    C(int x) : m_x{x} {}
    int m_x;
};

namespace detail {
    template<class Type, std::size_t...Is, class...Args>
    auto generate_n_with(std::index_sequence<Is...>, const Args&...args)
    {
        return std::array<Type, sizeof...(Is)> {
            {(void(Is), Type { args... })...} // Or replace '{ args... }' with '( args... )'; see in comments below.
        };
    }
}

template<class Type, std::size_t N, class...Args>
auto generate_n_with(const Args&...args)
{
    return detail::generate_n_with<Type>(std::make_index_sequence<N>(), args...);
}

int main()
{
    auto a = generate_n_with<C, 3>(1);
    for (auto&& c : a)
    {
        std::cout << c.m_x << std::endl;
    }
}

results:

1
1
1

I want to guarantee no copies prior to c++17

The you would need to generate into a vector:

template<class Container, class...Args>
auto emplace_n(Container& c, std::size_t n, Args const&...args)
{
    c.reserve(n);
    while(n--) {
        c.emplace_back(args...);
    }
};

used like this:

std::vector<C> v2;
emplace_n(v2, 3, 1);
Danra
  • 9,546
  • 5
  • 59
  • 117
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Until c++17 there is no guaranteed copy elision so this might actually cause copy of an array generated in `generate_n_with`, no? :) – W.F. Dec 27 '16 at 16:02
  • Yes I know that's why +1 but I was just wonder do you think there is a way to initialize the array inline somehow? – W.F. Dec 27 '16 at 16:06
  • 1
    @W.F. for a vector, yes. For an array, no. Will edit to include the vector version. – Richard Hodges Dec 27 '16 at 16:07
  • Can someone explain what's going on in the aggregate initialization of the array in `generate_n_with`? `{(void(Is), Type { args... })...}` – Danra Dec 28 '16 at 10:42
  • Also, in the case that the array is an instance variable of a known type, is there a way to avoid repeating its template arguments when constructing the array using generate_n_with()? – Danra Dec 28 '16 at 10:54
  • @Danra you could certainly deduce the element type if the first argument was of type T. – Richard Hodges Dec 28 '16 at 14:00
  • 1
    @Dana the above expansion depends on 2 thibgs. First is that the ... operator's scope is limited to expanding inside braces or brackets. The second is the use of the comma operator to force expansion of Is with no side effects. It means that Is is only used to drive the expansion of the outer ... – Richard Hodges Dec 28 '16 at 14:03
  • @RichardHodges One caveat - I tried using the above to initialize an `std::array` of `std::vector` using the constructor from size and value - it didn't work, since the compiler thought I'm trying to initialize each `vector` with an initializer list of two doubles, and complained about needing an explicit cast from `int` (the size parameter) to `double`. (Similar error to one you might get get when just trying to use curly braces to initialize a plain `vector` with this ctor). Changing `Type { args... }` to `Type ( args... )` in `generate_n_with` made it work for this case. – Danra Jan 01 '17 at 15:48
  • @Danra great, thanks for the feedback. On balance I think the round brackets are better all round for this solution. – Richard Hodges Jan 02 '17 at 07:40
1

You can construct a sequence of elements using an std::index_sequence<...> and expand that into the initializers of an array. I don't know of any approach avoiding an auxiliary function, though. Here is an example:

#include <iterator>
#include <algorithm>
#include <iostream>

struct S {
    int value;
    S(int value): value(value) {}
};
std::ostream& operator<< (std::ostream& out, S const& s) {
    return out << s.value;
}

#include <array>
#include <iterator>
#include <algorithm>
#include <iostream>

struct S {
    int value;
    S(int value): value(value) {}
};
std::ostream& operator<< (std::ostream& out, S const& s) {
    return out << s.value;
}

template <typename T, std::size_t... I>
std::array<T, sizeof...(I)> fill_aux(T value, std::index_sequence<I...>)
{
    return std::array<T, sizeof...(I)>{ (void(I), value)... };
}
template <std::size_t N, typename T>
std::array<T, N> fill(T value) {
    return fill_aux(value, std::make_index_sequence<N>());
}

int main()
{
    std::array<S, 10> array = fill<10>(S(17));
    std::copy(array.begin(), array.end(), std::ostream_iterator<S>(std::cout, " "));
}
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Looks to me like `fill(init)` (which returns an `std::array` of size `N` with elements constructed from `init`) would be a great addition to STL. Or, just add a constructor to `std::array` which takes `init`, similar to the one `std::vector` has (obviously without the `size` parameter). – Danra Dec 27 '16 at 15:49
1

By creating derived class, you can effectively create a new default value. It's a bit hackish, but may be less hackish than other solutions. Here's an example:

class C {
public:
   C() : C(0) {}
   C(int x) : m_x{x} {}
   int m_x;
};


template <int init>
struct CInit : C { CInit() : C(init) {} };

CInit<1> ar2[3];


const int initVal = 1;
CInit<initVal> ar3[3];

Another approach is to wrap your raw array inside a struct with a variadic constructor:

template <size_t n>
struct Array {
    C array[n];

    template <size_t... seq>
    Array(int init,std::index_sequence<seq...>)
    : array{(void(seq),init)...}
    {
    }

    Array(int init)
    : Array(init,std::make_index_sequence<n>())
    {
    }
};


const int initVal = 1;
Array<3> ar3_1(initVal);
const C (&ar3)[3] = ar3_1.array;
Vaughn Cato
  • 63,448
  • 5
  • 82
  • 132
0

Building on Richard's answer, it's also possible to define

template<class Type, std::size_t N, class...Args>
auto generate_n_with(const std::array<Type, N>&, const Args&...args)
{
    return detail::generate_n_with<Type>(std::make_index_sequence<N>(), args...);
};

Allowing you to enter the array as a parameter to make the code more dry in case you already know the type of the array, e.g.

class D {
public:
    D();
    std::array<int, 3> m_ar;
};

Allowing

D::D() : m_ar{generate_n_with{m_ar, 5}} {}

Instead of the less DRY

D::D() : m_ar{generate_n_with<int, 3>{5}} {}

P.S. maybe there's an even DRYer way without repeating m_ar twice?

Danra
  • 9,546
  • 5
  • 59
  • 117