如何正确初始化一个boost多维数组的对象?

5
我很惊讶地发现,boost::multi_array 似乎与 std::vector 不同,它似乎不会使用默认值或默认构造函数填充每个元素。我很难找到更多关于这方面的信息。
是否有一种方法可以使 multi_array 在每个元素中填充一个唯一的对象?
例如,考虑以下内容:
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();
      }
   }
}

这将导致输出结果为:
vector:
A()
A()
A()
n=0
n=1
n=2
multi:
A()
n=3
n=3
n=3
n=3

为什么multi_array的构造函数只被调用一次?如何使用A的默认构造函数填充multi_array以获取唯一对象?


2
请注意,std::vector 的行为取决于您使用的 C++ 版本!这是在 C++98 和 C++11 标准之间发生的变化之一。(在 C++98 中,第一个元素被构造,其余的则被复制,在 C++11 中则全部被构造)。multi_array 的行为与旧的 C++98 std::vector 保持一致,并且过去一直如此。也许需要一个更新的(一致的)版本的 multi_array - alfC
2个回答

4

要快速填充整个数组,可以使用fill_n¹函数:

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

使用boost的multi_array,你可以使用自己的内存缓冲区来获得相同的性能(std::uninitialized_copy是你的好朋友)。(实际上,你甚至可以在现有内存上映射数组视图,并保留现有值)。我已经在这里写了一个比较演示:pointers to a class in dynamically allocated boost multi_array, not compiling 在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);
}

使用multi_array_ref的变体是避免复制元素的示例(类似于std::vector在使用保留但未使用的未初始化内存时使用的优化)。
¹ 当然,对于唯一值,可以使用std::iotastd::generate

我认为这可能适用于std::array(当然也适用于普通的C/C++数组)。但是,boost::multi_array似乎确实会初始化其元素。在发布此问题后,我发现它只是以与std::vector不同的方式进行初始化。 - Corey
@Corey 我已经扩展了我的答案,包括一个演示,展示了如何使用multi_array_ref来进行原地元素构建。记住:过早的优化是万恶之源 :) - sehe
感谢提供如何使用预分配内存的示例;这可能很有用。实际上,在这种情况下,这并不是我的目标——我主要是因为没有看到默认构造函数被调用而感到困惑,并认为我做错了什么。...我正在考虑接受自己的答案,因为它展示了在开放问题中正在发生的事情,这对于将来找到这个问题的人可能很有用。 - Corey

1
在进一步研究后,我了解到两件事情:
1. `boost::multi_array` 使用拷贝构造函数将对象初始化到容器中,而不是默认构造函数。 2. 在 C++11 中用 `for (auto x : container)` 的方式循环遍历时,似乎(至少在 clang++ 3.5 中)会循环遍历容器元素的副本,而不是迭代器(或引用)。 修改原问题的示例以演示第一点。 添加一个拷贝构造函数(和相应的计数器),并使用 `auto& x` 作为对象循环的变量,而不是 `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();
       }
    }
 }

产生输出。
 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

修改上面的示例以演示第二点。

与本答案中上面相同的类定义,但从对象循环中删除 auto& x,并回到使用原始问题中所做的 auto x

    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();
       }
    }

生成输出,显示在print循环期间调用了复制构造函数,即使是向量中的元素也是如此。

 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

3
当然,for (auto x : c) 循环会复制变量。也许你可以尝试 for (auto& x: c) 来遍历 lvalues(编辑:哦,你注意到了。那么我不确定你为什么还把它指出来,好像这是关于多维数组的有趣之处)。 - sehe
@sehe 我并不是要将multi_array指出为特殊的东西,它只是原问题需要进行的更改之一。关于multi_array,重要的是它使用了复制构造函数,而不像vector那样使用默认构造函数。 - Corey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接