为什么C++会用零初始化std::vector,而不是std::array?

8

如果你不需要一个初始化为零的向量,那么将其初始化为零是不是浪费时间呢?

我尝试了这段代码:

#include <iostream>
#include <vector>
#include <array>

#define SIZE 10

int main()
{
#ifdef VECTOR

  std::vector<unsigned> arr(SIZE);

#else

  std::array<unsigned, SIZE> arr;

#endif // VECTOR

  for (unsigned n : arr)
    printf("%i ", n);
  printf("\n");

  return 0;
}

我收到了输出:

使用向量

$ g++ -std=c++11 -D VECTOR test.cpp -o test && ./test 
0 0 0 0 0 0 0 0 0 0 

使用数组
g++ -std=c++11  test.cpp -o test && ./test 
-129655920 32766 4196167 0 2 0 4196349 0 1136 0 

我也尝试使用clang++编译。

那么为什么是零呢?顺便问一下,我能声明一个未初始化的向量吗?


1
它们是在很多年之间设计的,所以也许思路发生了改变。std::arraystd::vector更好地遵循了“你不需要付出你不需要的代价”的原则。 - juanchopanza
1
std::vector: 因为默认构造函数参数见(2):http://en.cppreference.com/w/cpp/container/vector/vector “使用值value构造count个元素的容器。” - Richard Critten
2
如果您初始化数组 std::array<...> arr{};,则会发生这种情况。 - StoryTeller - Unslander Monica
5
@juanchopanza 是的,如果使用unsigned()进行默认初始化,则会将其默认初始化为0。如果您不想进行默认初始化,请不要指定大小并调用reserve以获取已初始化的空间。所有(8个)构造函数在值(或复制)初始化向量方面都是一致的。 - Richard Critten
1
@MoisesRojo 我所指的设计哲学是“你不需要的东西就不要付费”。因此,如果您不需要一个零初始化的数组(考虑使用向量动态数组),那么为什么要为它付费呢?标准C++没有为std::vector提供替代方案,但对于std::array则有。我认为这是一个设计缺陷,但也许在早期的C++中实现非零内置类型的清零太难了。 - juanchopanza
显示剩余6条评论
3个回答

5

声明向量的更常见的方法是不指定大小:

std::vector<unsigned> arr;

这不会为向量内容分配任何空间,也没有任何初始化开销。元素通常是通过.push_back()等方法动态添加的。如果您想分配内存,可以使用reserve()

arr.reserve(SIZE);

这不会初始化添加的元素,它们不包括在向量的 size() 中,并且试图读取它们是未定义的行为。与此相比,可以参考下面的示例:

arr.resize(SIZE);

这个函数会增长向量并初始化所有新元素。

另一方面,std::array 总是分配内存。它实现了大多数与 C 风格数组相同的行为,除了自动衰减为指针。这包括不默认初始化元素。


严肃吗?我认为几乎每个人都会使用size(),这意味着您需要浪费时间用零初始化向量,是或否。对吧?因为是的,push_back()提供了定义行为,但是...我不想使用push_back() - Moises Rojo
例如,我如何在范围[1, 10]内使用std :: iota,而不初始化vec,并使用std或boost函数? - Moises Rojo
@MoisesRojo:“像generate_n(back_inserter(v), 10, []{ static int x = 7; return x++; });这样的代码可能会。” - Zan Lynx

5
默认分配器进行零初始化。您可以使用不执行此操作的其他分配器。我编写了一个分配器,当可行时使用默认构造而不是初始化。更准确地说,它是一个称为ctor_allocator的分配器包装器。然后,我定义了一个vector模板。 dj:vector<unsigned> vec(10); 正好符合您的要求。它是一个std::vector<unsigned> (10),不会初始化为零。
--- libdj/vector.h ----
#include <libdj/allocator.h>
#include <vector>

namespace dj {
template<class T>
    using vector = std::vector<T, dj::ctor_allocator<T>>;
}

--- libdj/allocator.h  ----
#include <memory>

namespace dj {

template <typename T, typename A = std::allocator<T>>
    class ctor_allocator : public A 
    {
        using a_t = std::allocator_traits<A>;
    public:
        using A::A; // Inherit constructors from A

        template <typename U> struct rebind 
        {
            using other =
                ctor_allocator
                <  U, typename a_t::template rebind_alloc<U>  >;
        };

        template <typename U>
        void construct(U* ptr)
            noexcept(std::is_nothrow_default_constructible<U>::value) 
        {
            ::new(static_cast<void*>(ptr)) U;
        }

        template <typename U, typename...Args>
        void construct(U* ptr, Args&&... args) 
        {
            a_t::construct(static_cast<A&>(*this),
                ptr, std::forward<Args>(args)...);
        }
    };
}

你有什么建议吗? - Moises Rojo
评论不是用来解决编程问题的。也许我的代码使用了GCC 7.3不支持的C++11特性。我不使用GCC,也不知道它的任何信息。如果你无法解决问题,请将其作为一个新问题发布。 - Jive Dadson
程序编译通过且不会崩溃,但是使用零初始化向量。实际上,在 wandbox 中,如果向量的大小为 (75 >=) dj::vector,则会使用零初始化向量。 - Moises Rojo
在我的电脑上,无论大小如何,都要用零初始化向量。 - Moises Rojo
1
好的,分配器没有进行(或重新进行)零初始化。仅仅因为内存包含零并不意味着它们是在向量构造时放置的。可能操作系统在开始时清除进程内存,而您的测试代码正在使用自进程启动以来从未使用过的内存。Wandbox版本中75及以上的位置为零但较低的位置不为零的原因无疑是当您分配少量内存时,您得到的是之前使用过的内存,但是当您分配更大的内存时则不是这样。 - Jive Dadson
显示剩余6条评论

3
假设我们有一个类:
class MyClass {
    int value;

public:
    MyClass() {
        value = 42;
    }
    // other code
};

std::vector<MyClass> arr(10);会默认构造10个MyClass的实例,它们都拥有value = 42

但是,如果没有默认构造这10个副本,那么如果我写了arr[0].some_function(),就会出现问题:因为MyClass的构造函数尚未运行,所以类的不变量还没有设置。我可能已经在some_function()的实现中假定value == 42,但由于构造函数尚未运行,value具有某些不确定的值。这将是一个错误。

这就是为什么在C++中存在对象生命周期的概念。对象在构造函数被调用之前不存在,在析构函数被调用之后停止存在。std::vector<MyClass> arr(10);对每个元素调用默认构造函数,以便所有对象都存在。

需要注意的是,std::array有点特殊,因为它是按照聚合初始化规则进行初始化的。这意味着std::array<MyClass, 10> arr;也会默认构造10个MyClass的副本,所有副本的value都等于42。但对于非类类型(如unsigned),其值将是不确定的。


有一种避免调用所有默认构造函数的方法:std::vector::reserve。如果我写:

std::vector<MyClass> arr;
arr.reserve(10);

这个向量将分配其支持数组以容纳10个MyClass,并且不会调用默认构造函数。但现在我不能写arr[0]arr[5];这些将超出arr的边界访问(arr.size()仍为0,即使支持数组有更多元素)。要初始化值,我必须调用push_backemplace_back
arr.push_back(MyClass{});

通常这是正确的方法。例如,如果我想使用std::generate_nstd::back_inserter一起来使用std::rand中的随机值填充arr

std::vector<unsigned> arr;
arr.reserve(10);
std::generate_n(std::back_inserter(arr), 10, std::rand);

值得注意的是,如果我已经有了一个包含我想要的arr值的容器,我可以在构造函数中直接传递begin()/end()
std::vector<unsigned> arr{values.begin(), values.end()};

3
我很惊讶这个被选中了,因为这引出了一个问题:“为什么 std::array 不像这样表现?为什么 std::vector 不能像 std::array 这样表现?”而且这个问题听起来非常类似于 OP 正在问的问题。 - juanchopanza
@juanchopanza 这是真的,但我不确定我想在这个答案中深入探讨这个问题。我打算给OP一些关于为什么std::vector会以这种方式行事的直觉。 - Justin
1
问题... "为什么...?" R: 因为向量调用默认构造函数,它是隐式的(或者我认为是这样),而数组不会调用它,这解决了这个问题。 - Moises Rojo
顺便问一下 @Justin,你知道如何使用 beginend(在emacs中是 begin()/end())初始化一个n维向量吗?就像这个链接中的例子。 - Moises Rojo
@juanchopanza 再思考一下,我进行了编辑,加入了一个简短的解释,说明为什么 std::array 会表现出它的行为方式。仅根据我之前给出的解释,这种行为确实很奇怪。 - Justin
显示剩余2条评论

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