1

I am trying to write a function in order to generate arbitrarily nested vectors and initialize with the given specific value in C++. For example, auto test_vector = n_dim_vector_generator<2, long double>(static_cast<long double>(1), 1); is expected to create a "test_vector" object which type is std::vector<std::vector<long double>>. The content of this test_vector should as same as the following code.

    std::vector<long double> vector1;
    vector1.push_back(1);
    std::vector<std::vector<long double>> test_vector;
    test_vector.push_back(vector1);

The more complex usage of the n_dim_vector_generator function:

auto test_vector2 = n_dim_vector_generator<15, long double>(static_cast<long double>(2), 3);

In this case, the parameter static_cast<long double>(2) is as the data in vectors and the number 3 is as the push times. So, the content of this test_vector2 should as same as the following code.

    std::vector<long double> vector1;
    vector1.push_back(static_cast<long double>(2));
    vector1.push_back(static_cast<long double>(2));
    vector1.push_back(static_cast<long double>(2));
    std::vector<std::vector<long double>> vector2;
    vector2.push_back(vector1);
    vector2.push_back(vector1);
    vector2.push_back(vector1);
    std::vector<std::vector<std::vector<long double>>> vector3;
    vector3.push_back(vector2);
    vector3.push_back(vector2);
    vector3.push_back(vector2);
    //...Totally repeat 15 times in order to create test_vector2
    std::vector<...std::vector<long double>> test_vector2;
    test_vector2.push_back(vector14);
    test_vector2.push_back(vector14);
    test_vector2.push_back(vector14);

The detail to implement n_dim_vector_generator function is as follows.

#include <iostream>
#include <vector>

template <typename T, std::size_t N>
struct n_dim_vector_type;

template <typename T>
struct n_dim_vector_type<T, 0> {
    using type = T;
};

template <typename T, std::size_t N>
struct n_dim_vector_type {
    using type = std::vector<typename n_dim_vector_type<T, N - 1>::type>;
};


template<std::size_t N, typename T>
typename n_dim_vector_type<T,N>::type n_dim_vector_generator(T t, unsigned int);

template <std::size_t N, typename T>
typename n_dim_vector_type<T, N>::type n_dim_vector_generator<N, T>(T input_data, unsigned int push_back_times) {
    if (N == 0)
    {
        return std::move(input_data);
    }
    typename n_dim_vector_type<T, N>::type return_data;
    for (size_t loop_number = 0; loop_number < push_back_times; loop_number++)
    {
        return_data.push_back(n_dim_vector_generator<N - 1, T>(input_data, push_back_times));
    }
    return return_data;
}

As a result, I got an error 'return': cannot convert from 'long double' to 'std::vector<std::vector<long double,std::allocator<long double>>,std::allocator<std::vector<long double,std::allocator<long double>>>>' I know that it caused by if (N == 0) block which is as the terminate condition to recursive structure. However, if I tried to edit the terminate condition into separate form.

template <typename T>
inline T n_dim_vector_generator<0, T>(T input_data, unsigned int push_back_times) {
    return std::move(input_data);
}

template <std::size_t N, typename T>
typename n_dim_vector_type<T, N>::type n_dim_vector_generator<N, T>(T input_data, unsigned int push_back_times) {
    typename n_dim_vector_type<T, N>::type return_data;
    for (size_t loop_number = 0; loop_number < push_back_times; loop_number++)
    {
        return_data.push_back(n_dim_vector_generator<N - 1, T>(input_data, push_back_times));
    }
    return return_data;
}

The error 'n_dim_vector_generator': illegal use of explicit template arguments happened. Is there any better solution to this problem?

The develop environment is in Windows 10 1909 with Microsoft Visual Studio Enterprise 2019 Version 16.4.3

JimmyHu
  • 403
  • 1
  • 8
  • 20
  • 2
    My advice is to not do this. Multidimensional vectors lack data locality and can become a performance bottleneck. It is more efficient, and easier to implement if you wrap a 1d vector in a class that can fake it has multiple dimensions using math. Something along the lines of this: https://stackoverflow.com/a/43358434/4342498 – NathanOliver Jan 29 '20 at 15:46
  • To simplify what @NathanOliver said, you might already know that arr2D[i][j] is similar to arr1D[i * k + j] by simply modifying the control loop. This basic idea can be extended to N dimensions. – Ardent Coder Jan 29 '20 at 16:06
  • Thanks to @NathanOliver for prompt reply. Due to some reasons on the project which I am working on. I want to design this kind of function for testing purpose. – JimmyHu Jan 29 '20 at 16:40
  • Thanks to @Ardent for prompt reply and detailed description. Considering the characteristics and convenience in usage of std::vector, I think that std::vector is safer in memory management and more function-completed than arr2D. I know that using smart pointer could implement memory-safe arr2D, but it is hard to deal with multi-dimensional allocation in this way. – JimmyHu Jan 29 '20 at 16:52
  • To clarify, are you looking for this strange 1-element multi-dimensional vector specifically, or are you looking for a general multi-dimensional data structure? – parktomatomi Jan 29 '20 at 19:16
  • @parktomatomi The function n_dim_vector_generator should generate arbitrary multi-dimensional data structure base on the input parameters. The mentioned example illustrate the simple usage. In more complex usage: `auto test_vector = n_dim_vector_generator<15, long double>(static_cast(2), 3);` – JimmyHu Jan 30 '20 at 00:43
  • @JimmyHu can you clarify what the meaning of 2 and 3 in your example are? – parktomatomi Jan 30 '20 at 01:08
  • @parktomatomi To describe the meaning of parameter `static_cast(2)` and `3`, I already edit the content of this question above. Please check. Thank you. – JimmyHu Jan 30 '20 at 05:42
  • Thanks for editing. I updated my answer – parktomatomi Jan 30 '20 at 17:12
  • Thanks to @parktomatomi for providing the answer. It's really a solution I expected. – JimmyHu Jan 30 '20 at 17:47

1 Answers1

2

To achieve your specific mapping of:

auto test_vector = n_dim_vector_generator<2, long double>(2, 3)

to a 3x3 vector filled with 2's, your template can be a bit simpler if you take advantage of this vector constructor:

std::vector<std::vector<T>>(COUNT, std::vector<T>(...))

Since vector is copyable, this will fill COUNT slots with a different copy of the vector. So...

template <size_t N, typename T>
struct n_dim_vector_generator {
    using type = std::vector<typename n_dim_vector_generator<N-1, T>::type>;
    type operator()(T value, size_t size) {
        return type(size, n_dim_vector_generator<N-1, T>{}(value, size));
    }
};

template <typename T>
struct n_dim_vector_generator<0, T> {
    using type = T;
    type operator()(T value, size_t size) {
        return value;
    }
};

usage:

auto test_vector = n_dim_vector_generator<2, long double>{}(2, 3);

Demo: https://godbolt.org/z/eiDAUG


For the record, to address some concerns from the comments, C++ has an idiomatic, initializable, contiguous-memory class equivalent of a multi-dimension C array: a nested std::array:

std::array<std::array<long double, COLUMNS>, ROWS> test_array = { /*...*/ };

for (auto& row : test_array)
    for (auto cell : row)
        std::cout << cell << std::endl;

If you wanted to reduce the boilerplate to declare one, you can use a struct for that:

template <typename T, size_t... N>
struct multi_array;

template <typename T, size_t NFirst, size_t... N>
struct multi_array<T, NFirst, N...> {
    using type = std::array<typename multi_array<T, N...>::type, NFirst>;
};

template <typename T, size_t NLast>
struct multi_array<T, NLast> {
    using type = std::array<T, NLast>;
};

template <typename T, size_t... N>
using multi_array_t = typename multi_array<T, N...>::type;

Then to use:

multi_array_t<long double, ROWS, COLUMNS> test_array = { /*...*/ };

for (auto& row : test_array)
    for (auto cell : row)
        std::cout << cell << std::endl;

This is allocated on the stack, like a C array. That will eat up your stack space for a big array of course. But you can make a decorator range around std::unique_ptr to make a pointer to one a bit easier to access:

template <typename T, size_t... N>
struct dynamic_multi_array : std::unique_ptr<multi_array_t<T, N...>> {
    using std::unique_ptr<multi_array_t<T, N...>>::unique_ptr;
    constexpr typename multi_array_t<T, N...>::value_type& operator [](size_t index) { return (**this)[index]; }
    constexpr const typename multi_array_t<T, N...>::value_type& operator [](size_t index) const { return (**this)[index]; }
    constexpr typename multi_array_t<T, N...>::iterator begin() { return (**this).begin(); }
    constexpr typename multi_array_t<T, N...>::iterator end() { return (**this).end(); }
    constexpr typename multi_array_t<T, N...>::const_iterator begin() const { return (**this).begin(); }
    constexpr typename multi_array_t<T, N...>::const_iterator end() const { return (**this).end(); }
    constexpr typename multi_array_t<T, N...>::const_iterator cbegin() const { return (**this).cbegin(); }
    constexpr typename multi_array_t<T, N...>::const_iterator cend() const { return (**this).cend(); }
    constexpr typename multi_array_t<T, N...>::size_type size() const { return (**this).size(); }
    constexpr bool empty() const { return (**this).empty(); }
    constexpr typename multi_array_t<T, N...>::value_type* data() { return (**this).data(); }
    constexpr const typename multi_array_t<T, N...>::value_type* data() const { return (**this).data(); }
};

(let the buyer beware if you use those methods with nullptr)

Then you can still brace-initialize a new expression and use it like a container:

dynamic_multi_array<long double, ROWS, COLUMNS> test_array {
    new multi_array_t<long double, ROWS, COLUMNS> { /* ... */ }
};

for (auto& row : test_array)
    for (auto cell : row)
        std::cout << cell << std::endl;

Demo: https://godbolt.org/z/lUwVE_

parktomatomi
  • 3,851
  • 1
  • 14
  • 18