4

I have been surprised to find that boost::multi_array seems to allocate its initial elements differently from, say, std::vector. It does not seem to fill each element with a unique element (using its default value or default constructor). I'm having trouble finding more information about this.

Is there a way to make the multi_array fill itself with a unique object at each element?

For example, consider the following:

static int num = 0;

struct A {
   int n;
   A() : n((::num)++) {
      std::cout << "A()" << std::endl;
   }
   virtual ~A() {}

   void print() {
      std::cout << "n=" << n << std::endl;
   }
};

int main() {
   std::cout << "vector:" << std::endl;
   std::vector<A> v(3);
   for (auto x : v) {
      x.print();
   }

   std::cout << "multi:" << std::endl;
   boost::multi_array<A, 2> m(boost::extents[2][2]);
   for (auto x : m) {
      for (auto y : x) {
         y.print();
      }
   }
}

This results in the output:

vector:
A()
A()
A()
n=0
n=1
n=2
multi:
A()
n=3
n=3
n=3
n=3

Why is the constructor called only once for the multi_array? How can the multi_array be "filled out" with unique objects (using A's default constructor)?

Corey
  • 1,845
  • 1
  • 12
  • 23
  • 2
    Please note that the behavior of `std::vector` depends on what version of C++ you are using! This is one of those things that changed between C++98 and C++11 standard. (in C++98 the first element is constructed and the rest are copied, in C++11 all are constructed). The behavior of `multi_array` is consistent with old C++98 `std::vector` and was consistent in the past. Maybe an updated (consistent) version of `multi_array` is needed. – alfC Jul 02 '15 at 18:40

2 Answers2

3

To quickly fill the whole array do something like fill_n¹:

std::fill_n(a.data(), a.num_elements(), 0);

With boost multi_array you can use a view to your own memory buffer to get the same performance (std::uninitialized_copy is your friend). (actually, you could even map an array view on existing memory, and you want to keep the existing values).

I've written a comparative demo about this here: pointers to a class in dynamically allocated boost multi_array, not compiling

Live On Coliru

#include <boost/multi_array.hpp>
#include <type_traits>
#include <memory>

struct octreenode { int a; int b; };

class world {
public:
    world(double x, double y, double z, int widtheast, int widthnorth, int height)
            : 
                originx(x), originy(y), originz(z), 
                chunkseast(widtheast), chunksnorth(widthnorth), chunksup(height)
    {
#define OPTION 4

#if OPTION == 1
        static_assert(std::is_trivially_destructible<octreenode>::value, "assumption made");
        //std::uninitialized_fill_n(chunk.data(), chunk.num_elements(), octreenode {1, 72});
        std::fill_n(chunk.data(), chunk.num_elements(), octreenode {1, 72});
#elif OPTION == 2
        for(auto a:chunk) for(auto b:a) for(auto&c:b) c = octreenode{1, 72};
#elif OPTION == 3
        for (index cz = 0; cz < chunksnorth; ++cz) {
            for (index cx = 0; cx < chunkseast; ++cx) {
                for (index cy = 0; cy < chunksup; ++cy) {
                    chunk[cz][cx][cy] = octreenode{1, 72};
                }
            }
        }
#elif OPTION == 4
        static_assert(std::is_trivially_destructible<octreenode>::value, "assumption made");
        for (index cz = 0; cz < chunksnorth; ++cz) {
            for (index cx = 0; cx < chunkseast; ++cx) {
                for (index cy = 0; cy < chunksup; ++cy) {
                    new (&chunk[cz][cx][cy]) octreenode{1, 72};
                }
            }
        }
#endif
        (void) originx, (void) originy, (void) originz, (void) chunksup, (void) chunkseast, (void) chunksnorth;
    }

private:
    double originx, originy, originz;
    int chunkseast, chunksnorth, chunksup;

#if 1
    typedef boost::multi_array<octreenode, 3> planetchunkarray; // a boost_multi for chunks
    typedef planetchunkarray::index index;
    planetchunkarray chunk{boost::extents[chunksnorth][chunkseast][chunksup]};
#else
    static_assert(boost::is_trivially_destructible<octreenode>::value, "assumption made");

    std::unique_ptr<octreenode[]> raw { new octreenode[chunksnorth*chunkseast*chunksup] };
    typedef boost::multi_array_ref<octreenode, 3> planetchunkarray;
    typedef planetchunkarray::index index;
    planetchunkarray chunk{raw.get(), boost::extents[chunksnorth][chunkseast][chunksup]};
#endif
};

int main() {
    world w(1,2,3,4,5,6);
}

The variant using multi_array_ref is an example of how to avoid copy-constructing the elements (it's akin to the optimization used by std::vector when it uses uninitialized memory for reserved but unused elements).


¹ Of course for unique values, use std::iota or std::generate

Community
  • 1
  • 1
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I think this may be true for `std::array` (certainly for plain C/C++ arrays). But `boost::multi_array` does seem to initialize its elements. It just does so differently than `std::vector`, as I found out after posting this question. – Corey Mar 04 '15 at 01:58
  • @Corey I've expanded my answer with some more information including a demo that shows how you might use `multi_array_ref` to get in-place element construction going. Remember: premature optimization is at the root of all evil :) – sehe Mar 04 '15 at 10:50
  • Thanks for the example of how to use pre-allocated memory; that may be useful. It actually wasn't my goal in this case -- I was mostly just confused why I wasn't seeing the default constructor being called and thought I was doing something wrong. ...I'm considering accepting my own answer because it demonstrates what's going on in the opening question, which could be useful for someone finding this question in the future. – Corey Mar 05 '15 at 04:19
1

So on further study, I learned two things:

  1. boost::multi_array uses the copy constructor to initialize objects into the container, not the default constructor.

  2. The for (auto x : container) way of looping in C++11 seems (at least with clang++ 3.5) to loop over copies of the container elements, rather than iterators (or references).

Modifying the original question's example to demonstrate point 1.

Adding a copy constructor (and corresponding counter), and using auto& x for the object loops rather than auto x:

 static int num = 0;
 static int cpy = 0;
 struct A {
    int n;
    int c;
    A() : n((::num)++), c(0) {
       std::cout << "A_def()" << std::endl;
    }
    A(const A& o) : n(0), c((::cpy)++) {
       std::cout << "A_cpy()" << std::endl;
    }
    virtual ~A() {}

    void print() {
       std::cout << "n=" << n << ",c=" << c << std::endl;
    }
 };

 int main() {
    std::cout << "vector:" << std::endl;
    std::vector<A> v(3);
    for (auto& x : v) {
       x.print();
    }

    std::cout << "multi:" << std::endl;
    boost::multi_array<A, 2> m(boost::extents[2][2]);

    for (auto x : m) {
       for (auto& y : x) {
          y.print();
       }
    }
 }

Produces the output

 vector:
 A_def()  // <- vector allocation starts
 A_def()
 A_def()
 n=0,c=0  // <- vector printing starts, using "for (auto& x)"
 n=1,c=0
 n=2,c=0
 multi:
 A_def()  // <- a temporary object for multi_array allocation
 A_cpy()  // <- multi_array allocation starts
 A_cpy()
 A_cpy()
 A_cpy()
 n=0,c=0  // <- multi_array prints starts, using "for (auto& y)"
 n=0,c=1
 n=0,c=2
 n=0,c=3

Modifying the example above to demonstrate point 2.

Same class definition as above in this answer, but removing the auto& x from the object loops, and going back to using auto x as done in the original question.

    std::cout << "vector:" << std::endl;
    std::vector<A> v(3);
    for (auto x : v) {
       x.print();
    }

    std::cout << "multi:" << std::endl;
    boost::multi_array<A, 2> m(boost::extents[2][2]);

    for (auto x : m) {
       for (auto y : x) {
          y.print();
       }
    }

Produces output that shows the copy constructor gets called during the print loops, even for elements in the vector.

 vector:
 A_def()  // <- vector allocation starts
 A_def()
 A_def()
 A_cpy()  // <- vector printing starts, using "for (auto x)"
 n=0,c=0
 A_cpy()
 n=0,c=1
 A_cpy()
 n=0,c=2
 multi:
 A_def()  // <- a temporary object for multi_array allocation
 A_cpy()  // <- multi_array allocation starts
 A_cpy()
 A_cpy()
 A_cpy()
 A_cpy()  // <- multi_array printing starts, using "for (auto y)"
 n=0,c=7
 A_cpy()
 n=0,c=8
 A_cpy()
 n=0,c=9
 A_cpy()
 n=0,c=10
Corey
  • 1,845
  • 1
  • 12
  • 23
  • 2
    Derp. Of course `for (auto x : c)` loops over copies. Perhaps you could try `for (auto& x: c)` to loop over lvalues (Edit: ah you noticed. Then I'm not sure why you still point it out as if it was something interesting about multi array) – sehe Mar 04 '15 at 09:46
  • @sehe I didn't mean to point it out as something special to `multi_array`, it was just one of the changes that needed to be made to the original question. Regarding `multi_array`, the important bit is that it uses the copy constructor, not the default one like `vector` does. – Corey Mar 05 '15 at 04:11