在 C++11 和 Boost.Container 下 vector::resize(size_type n) 的行为是
Is this behavior of vector::resize(size_type n) under C++11 and Boost.Container correct?
我有一个C++03应用程序,其中std::vector<T>
类型始终用作临时缓冲区。因此,它们通常使用std::vector<T>::resize()
调整大小,以确保它们足够大,以便在使用前容纳所需的数据。这个函数的C++03原型实际上是:
void resize(size_type n, value_type val = value_type());
所以实际上在调用resize()
时,向量是通过添加适当数量的val
副本来放大的。但是,通常我只需要知道vector
足够大以容纳我需要的数据;我不需要用任何值初始化它。复制构造新值只是浪费时间。
C++11 来救援(我认为):在其规范中,它将resize()
拆分为两个重载:
void resize(size_type n); // value initialization
void resize(size_type n, const value_type &val); // initialization via copy
这非常符合C++的理念:只为你想要的东西付费。不过,正如我所指出的,我的应用程序不能使用 C++11,所以当我遇到 Boost.Container 库时,我很高兴,该库在其文档中指示对此功能的支持。具体来说,boost::container::vector<T>
实际上有三个重载resize()
:
void resize(size_type n); // value initialization
void resize(size_type n, default_init_t); // default initialization
void resize(size_type n, const value_type &val); // initialization via copy
为了验证我是否理解了一切,我做了一个快速测试来验证 C++11std::vector<T>
和boost::container::vector<T>
的行为:
#include <boost/container/vector.hpp>
#include <iostream>
#include <vector>
using namespace std;
namespace bc = boost::container;
template <typename VecType>
void init_vec(VecType &v)
{
// fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for (size_t i = 0; i < 10; ++i) v.push_back(i);
// chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
// should remain in memory
v.resize(5);
}
template <typename VecType>
void print_vec(const char *label, VecType &v)
{
cout << label << ": ";
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout << endl;
}
int main()
{
// instantiate a vector of each type that we're going to test
std::vector<int> std_vec;
bc::vector<int> boost_vec;
bc::vector<int> boost_vec_default;
// fill each vector in the same way
init_vec(std_vec);
init_vec(boost_vec);
init_vec(boost_vec_default);
// now resize each vector to 10 elements in ways that *should* avoid reinitializing the new elements
std_vec.resize(10);
boost_vec.resize(10);
boost_vec_default.resize(10, bc::default_init);
// print each one out
print_vec("std", std_vec);
print_vec("boost", boost_vec);
print_vec("boost w/default", boost_vec_default);
}
在 C++03 模式下使用g++
4.8.1 编译它,如下所示:
g++ vectest.cc
./a.out
生成以下输出:
std: 0 1 2 3 4 0 0 0 0 0
boost: 0 1 2 3 4 0 0 0 0 0
boost w/default: 0 1 2 3 4 5 6 7 8 9
这并不奇怪。我希望 C++03std::vector<T>
用零初始化最后 5 个元素。我甚至可以说服自己为什么boost::container::vector<T>
在做同样的事情(我假设它在C++03模式下模拟C++03行为)。只有当我专门要求默认初始化时,我才得到了我想要的效果。但是,当我以 C++11 模式重建时,如下所示:
g++ vectest.cc -std=c++11
./a.out
我得到这些结果:
std: 0 1 2 3 4 0 0 0 0 0
boost: 0 1 2 3 4 0 0 0 0 0
boost w/default: 0 1 2 3 4 5 6 7 8 9
一模一样!这就引出了我的问题:
在这种情况下,我认为我应该从三个测试中的每一个中看到相同的结果是错误的吗?这似乎表明std::vector<T>
接口更改实际上没有任何影响,因为在最终调用resize()
中添加的 5 个元素在前两种情况下仍然用零初始化。
不是答案,而是霍华德的冗长附录:我使用了一个分配器适配器,它的工作原理与霍华德的分配器基本相同,但更安全,因为
- 它只介入值初始化,而不是所有初始化,
- 它正确地默认初始化。
// Allocator adaptor that interposes construct() calls to
// convert value initialization into default initialization.
template <typename T, typename A=std::allocator<T>>
class default_init_allocator : public A {
typedef std::allocator_traits<A> a_t;
public:
template <typename U> struct rebind {
using other =
default_init_allocator<
U, typename a_t::template rebind_alloc<U>
>;
};
using A::A;
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)...);
}
};
与 C++11resize
签名的功能差异很小,但您的测试不会公开它。 考虑这个类似的测试:
#include <iostream>
#include <vector>
struct X
{
X() {std::cout << "X()n";}
X(const X&) {std::cout << "X(const X&)n";}
};
int
main()
{
std::vector<X> v;
v.resize(5);
}
在 C++03 下,这将打印:
X()
X(const X&)
X(const X&)
X(const X&)
X(const X&)
X(const X&)
但在 C++11 下,它打印:
X()
X()
X()
X()
X()
此更改的动机是更好地支持vector
中的不可复制(仅移动)类型。 大多数情况下,包括您的情况,此更改没有区别。
有一种方法可以使用自定义分配器(编译器可能支持也可能不支持)来完成 C++11 中所需的内容:
#include <iostream>
#include <vector>
using namespace std;
template <class T>
class no_init_alloc
: public std::allocator<T>
{
public:
using std::allocator<T>::allocator;
template <class U, class... Args> void construct(U*, Args&&...) {}
};
template <typename VecType>
void init_vec(VecType &v)
{
// fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v.resize(10);
for (size_t i = 0; i < 10; ++i) v[i] = i; // Note this change!!!
// chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
// should remain in memory
v.resize(5);
}
template <typename VecType>
void print_vec(const char *label, VecType &v)
{
cout << label << ": ";
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout << endl;
}
int
main()
{
std::vector<int, no_init_alloc<int>> std_vec;
init_vec(std_vec);
std_vec.resize(10);
print_vec("std", std_vec);
}
哪个应该输出:
std: 0 1 2 3 4 5 6 7 8 9
no_init_alloc
只是拒绝进行任何初始化,这对int
来说很好,留下一个未指定的值。 我不得不更改您的init_vec
以使用赋值进行初始化,而不是使用构造。 因此,如果您不小心,这可能会很危险/令人困惑。 但是,它确实避免了不必要的初始化。
未初始化的值
您可能已经通过创建适当的类初始化了值。 如下:
class uninitializedInt
{
public:
uninitializedInt() {};
uninitializedInt(int i) : i(i) {};
operator int () const { return i; }
private:
int i;
};
输出与"提升/默认值"相同。
或者创建一个自定义分配器,将construct
和destroy
作为 nop。
拆分resize
原型
如果void std::vector<T>::resize(size_type n)
执行void bc::vector<T>::resize(size_type n, default_init_t)
所做的事情,那么许多旧的有效代码就会中断......
resize()
的拆分允许将"仅移动"类的向量调整如下大小:
class moveOnlyInt
{
public:
moveOnlyInt() = default;
moveOnlyInt(int i) : i(i) {};
moveOnlyInt(const moveOnlyInt&) = delete;
moveOnlyInt(moveOnlyInt&&) = default;
moveOnlyInt& operator=(const moveOnlyInt&) = delete;
moveOnlyInt& operator=(moveOnlyInt&&) = default;
operator int () const { return i; }
private:
int i;
};
实际上在调用 resize() 时,通过添加适当数量的 val 副本来放大向量。 但是,通常,我只需要知道向量足够大以容纳我需要的数据;我不需要用任何值初始化它。复制构造新值只是浪费时间。
不,不是真的。拥有一个未实际构造的元素容器是没有意义的。我不确定除了零之外,你希望看到什么。未指定/未初始化的元素?这不是值初始化的意思。
如果你需要N个元素,那么你应该有N个正确构造的元素,这就是std::vector::resize
所做的。值初始化将零初始化一个对象,没有要调用的默认构造函数,所以实际上它与你似乎想要的相反,这是更少的安全性和初始化而不是更多。
我建议你真正追求的是std::vector::reserve
.
这似乎表明
std::vector<T>
界面更改实际上没有任何影响。
它当然有效果,只是不是你要找的那个。新的resize
重载是为了方便起见,这样当您只需要默认甚至值初始化时,您就不必构建自己的临时。这不是容器工作方式的根本变化,而是它们始终保存有效的类型实例†。
†有效,但如果您从它们移动,则处于未指定的状态!
int
的值初始化产生 0。
int
的默认初始化根本不初始化值 - 它只是保留内存中的任何内容。
要么resize(10)
分配的内存没有被resize(5)
释放,要么相同的内存块被重复使用。无论哪种方式,您最终都会留下先前的内容。
如果您想使用带有标准分配器的向量,这在 C++11 中不起作用吗?
namespace{
struct Uninitialised {};
template<typename T>
template<typename U>
std::allocator<T>::construct(U* , Uninitialised&&)
{
/*do nothing*/
};
}
template<typename T>
void resize_uninitialised(std::vector<T>& vec,
std::vector<T>::size_type size)
{
const Uninitialised* p = nullptr;
auto cur_size = vec.size();
if(size <= cur_size)
return;
vec.reserve(size);
//this should optimise to vec.m_size += (size - cur_size);
//one cannot help thinking there must be simpler ways to do that.
vec.insert(vec.end(), p, p + (size - cur_size));
};
关于凯西回答的小说明:
正如 Casey 在 1. 下指出的那样,上面的代码只插入值初始化。我不知道这是否意味着以下内容 - 但至少对我来说,这并不明显。
上面的代码仅避免了std::vector<T, default_init_allocator<T>>
中纯旧数据类型[POD] edT
的初始化。
这是否真的避免了运行时开销,但目前我不知道。也许其他人可以为我回答这个问题。
如果有人想测试这个,我将在下面附加我用于实际测试的代码。我可能应该补充一点,我使用了set(CMAKE_CXX_STANDARD 11)
和 MinGW 8.1.0 64 位C++编译器。
// ----------------------------------------------------
#include <iostream>
#include <memory>
#include <vector>
// ----------------------------------------------------
// forward declarations
class Blub;
std::ostream & operator<<(std::ostream &os, Blub const & blub);
class Blub
{
static unsigned instanceCounter_;
public:
static unsigned constexpr NO_SOURCE = -1;
// default constructor
Blub()
: instance(instanceCounter_)
, instanceSource(NO_SOURCE)
{
std::cout << "default constructor: " << *this << std::endl;
++instanceCounter_;
}
// destructor
~Blub()
{
--instanceCounter_;
std::cout << "destructor: " << *this << std::endl;
}
// copy constructor
Blub(Blub const &other)
: instance(instanceCounter_)
, instanceSource(other.instance)
{
std::cout << "copy constructor: " << *this << std::endl;
++instanceCounter_;
}
// move constructor
Blub(Blub &&other)
: instance(std::move(other.instance))
, instanceSource(std::move(other.instanceSource))
{
std::cout << "move constructor: " << *this << std::endl;
}
// copy assignment
Blub & operator=(Blub const &other)
{
instanceSource = other.instance;
std::cout << "copy assignment: " << *this << std::endl;
return (*this);
}
// move assignment
Blub & operator=(Blub &&other)
{
instance = std::move(other.instance);
instanceSource = std::move(other.instanceSource);
std::cout << "move assignment: " << *this << std::endl;
return (*this);
}
unsigned instance;
unsigned instanceSource;
};
unsigned Blub::instanceCounter_;
std::ostream & operator<<(std::ostream &os, Blub const & blub)
{
os << "Blub " << blub.instance;
if (Blub::NO_SOURCE != blub.instanceSource)
{
os << " [from " << blub.instanceSource << "]";
}
return os;
}
// ----------------------------------------------------
// Allocator adaptor that interposes construct() calls to
// convert value initialization into default initialization.
template <typename T, typename A = std::allocator<T>>
class default_init_allocator : public A
{
typedef std::allocator_traits<A> a_t;
public:
template <typename U> struct rebind
{
using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
};
using A::A;
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)...);
}
};
// ----------------------------------------------------
template <typename VecType>
void print_vec(const char *label, VecType &v)
{
std::cout << label << ": ";
for (size_t i = 0; i < v.size(); ++i)
{
std::cout << v[i] << " ";
}
std::cout << std::endl;
}
// ----------------------------------------------------
int main()
{
{
std::cout << "POD:" << std::endl;
std::vector<int, default_init_allocator<int>> vec(10);
// fill with values
for (size_t i = 0; i < 10; ++i)
{
vec[i] = i;
}
print_vec("initialized", vec);
vec.resize(5);
// this should not change value 5 to 9 in memory
vec.resize(10);
print_vec("resized", vec);
}
std::cout << std::endl;
{
std::cout << "C++ class:" << std::endl;
std::vector<Blub, default_init_allocator<Blub>> vec(10);
print_vec("initialized", vec);
vec.resize(5);
vec.resize(10);
print_vec("resized", vec);
}
}
// ----------------------------------------------------
我从中得到的输出是:
POD:
initialized: 0 1 2 3 4 5 6 7 8 9
resized: 0 1 2 3 4 5 6 7 8 9
C++ class:
default constructor: Blub 0
default constructor: Blub 1
default constructor: Blub 2
default constructor: Blub 3
default constructor: Blub 4
default constructor: Blub 5
default constructor: Blub 6
default constructor: Blub 7
default constructor: Blub 8
default constructor: Blub 9
initialized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9
destructor: Blub 5
destructor: Blub 6
destructor: Blub 7
destructor: Blub 8
destructor: Blub 9
default constructor: Blub 5
default constructor: Blub 6
default constructor: Blub 7
default constructor: Blub 8
default constructor: Blub 9
resized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9
destructor: Blub 0
destructor: Blub 1
destructor: Blub 2
destructor: Blub 3
destructor: Blub 4
destructor: Blub 5
destructor: Blub 6
destructor: Blub 7
destructor: Blub 8
destructor: Blub 9
PS:这个完整的答案只是出现了,因为我还没有被允许发表评论。否则,我可能只会要求 Casey 用注释扩展上面的答案,这基本上仅适用于 POD。
- C++,OpenCV,尝试显示图像时"OpenCV(4.3.0) Error: Assertion failed (size.width>0 && size.height>0)"此错误
- 大于65535的C++数组[size]引发不一致的溢出
- 为什么(-1)%vector::size()总是返回0
- 在for循环中使用auto vs decltype(vec.size())来处理字符串的向量
- 使用 [] 运算符时"binding reference of type discards qualifiers"
- 在 QVector<std::unique_ptr 上使用 std::find<Type>>
- 循环中的条件:为什么每次都调用strlen(),而vector.size()只调用一次
- 在类型和包装器之间reinterpret_cast是否安全<Type>?
- 使用 cmake 的 Linux 终端上的"Conversion to non-scalar type is requested"错误
- 控制到达非空函数clang(-Wreturn-type)的末尾
- 为什么这个 std::queue/指向结构的指针列表直到 List.Size() == 0 才释放内存?
- 警告的原因是什么:"when type is in parentheses, array cannot have dynamic size"?
- 调用 list.size() 方法给出错误"expression must have class type"
- vtkTypeTraits.h 错误:"Type long is not 4 or 8 bytes in size."
- C++/ASM - "Operand size conflict" , "Improper operand type"
- 提升精神语法错误 - "no type named ‘size’ in ‘struct boost::spirit::unused_type’"
- 如何在 c++11 中使用容器 std::array<type, size> 用于多维数组?
- OpenCV 错误:断言失败 (src1.size == dst.size && dst.type() == CV_8U) in cvInRangeS
- "error: request for member ‘size’ in ‘a’, which is of pointer type"但我不认为这是一个指针
- new(size, value) Type[0]返回的指针是否合法,是否可以用来构建数组?