重新分配 std::vector 对象时指向内部数据结构的指针的有效性
Validity of pointers to internal data structure when reallocating a std::vector object
假设有一个 class A 包含一个整数向量。现在假设创建了 A 的向量。例如,如果由于 push_back 而发生 A 对象的重新分配(因此向量对象被移动),指向 int 本身的指针是否仍然有效?这个有保证吗?
澄清一下:
class A {
public:
A() {};
std::vector<int> a = {1,2,3,4,5,6,7,8,9};
};
int main()
{
std::vector<A> aVec(2);
int *x1 = &(aVec[1].a[2]);
A *x2 = &aVec[1];
std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";
aVec.resize(30);
int *y1 = &(aVec[1].a[2]);
A *y2 = &aVec[1];
std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";
}
运行这段代码得到:
0x1810088 - 0x1810028 - 2
0x1810088 - 0x18100c8 - 30
所以它表明指针仍然有效。但我想确保这是有保证的,而不仅仅是偶然的事情。我倾向于说这是有保证的,因为向量的内部数据是动态分配的,但是,再次,只是想检查一下。
我看过这里 [Iterator invalidation rules] 但它没有考虑这种特定情况(即矢量对象本身的重新分配)。
更新:
我试过这个来检查我在 Jarod42 的回答的评论中写的内容:
std::vector<std::vector<int>> aVec(2, {1,2,3});
int *x1 = &(aVec[1][2]);
std::vector<int> *x2 = &aVec[1];
std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";
aVec.resize(30);
int *y1 = &(aVec[1][2]);
std::vector<int> *y2 = &aVec[1];
std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";
得到这个:
0x16f0098 - 0x16f0048 - 2
0x16f0098 - 0x16f00c8 - 30
这对我来说很奇怪。我期望 x2==y2.
这取决于您 A
的移动构造函数。
但是因为它 (*),它将使用 vector<int>
的移动构造函数作为 a
,并且根据 http://www.cplusplus.com/reference/vector/vector/vector/
[..] no elements are constructed (their ownership is directly transferred).
所以指向整数本身的指针仍然有效。
Edit: (*) A
应该是 noexcept
,std::vector
不保证是 noexcept
.
您的 A
元素将尽可能移动,并且 A
具有隐式移动构造函数和隐式移动赋值运算符,因此成员向量也将被移动。
现在,移动矢量不一定等同于 a.swap(b)
,因此如果您想要 保证,则不能依赖隐式移动函数;你可以自己写。
但是无论您自己保证还是通过查找特定标准库实现的代码获得保证,您都可以确信指向各个元素的指针和迭代器将保持有效:
[C++11: 23.2.1/8]:
The expression a.swap(b)
, for containers a
and b
of a standard container type other than array
, shall exchange the values of a
and b
without invoking any move, copy, or swap operations on the individual container elements. [..]
遗憾的是,这并不能保证。也就是说,所有 3 个当前实现(libc++、libstdc++ 和 VS-2015)似乎都可以保证这一点。问题是 A
的移动构造函数是否为 noexcept:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
A
的移动构造函数是编译器提供的,因此依赖于 std::vector<int>
的移动构造函数。如果 std::vector<int>
的移动构造函数是 noexcept,那么 A
的移动构造函数是 noexcept,否则不是。
当前草案 N4296 没有将 vector
的移动构造函数标记为 noexcept。但是它允许实现这样做。
这一行:
aVec.resize(30);
如果移动构造函数是 noexcept, 将使用 A
的移动构造函数,否则它将使用 A
的复制构造函数。如果它使用 A
的复制构造函数,则整数的位置将会改变。如果它使用 A
的移动构造函数,整数的位置将保持稳定。
libc++ 和 libstdc++ 将 vector
的移动构造函数标记为 noexcept。因此给 A
一个 noexcept 移动构造函数。
VS-2015 说 A
没有 noexcept 移动构造函数:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
不编译。
尽管如此,VS-2015 并未将整数重新分配到新地址,因此看起来它不符合 C++11 规范。
如果更改 libc++ headers 使得 vector
移动构造函数未标记为 noexcept,则整数确实会重新分配。
委员会最近的讨论表明,每个人都赞成标记 vector
noexcept 的移动构造函数(也许 basic_string
也是,但不是其他容器)。因此,未来的标准可能会保证您寻求的稳定性。与此同时,如果:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
编译,那么你有你的保证,否则你没有。
更新
更新中出现x2 != y2
的原因是这些是vector<vector<int>>
中vector<int>
的地址。这些内部元素必须找到一个新的(更大的)缓冲区来居住,就像内部元素是 int
一样。但与 int
不同的是,内部元素 vector<int>
可以使用移动构造函数移动到那里(int
必须复制)。但是无论是移动还是复制,内部元素的地址都必须改变(从旧的小缓冲区到新的大缓冲区)。此行为与问题的原始部分一致(内部元素也显示为更改地址)。
是的,LWG 2321 is involved, though not a contentious point. In my answer I've already assumed LWG 2321 已经过去了。除了过度急于调试迭代器无缘无故地(和错误地)使自己失效之外,真的没有其他方法可以让事情发生。 Non-debugging 迭代器永远不会失效,指针或引用也不会。
希望我能够轻松创建带有指向缓冲区的箭头的动画。那真的很清楚。我只是不知道如何在可用的时间内轻松地做到这一点。
假设有一个 class A 包含一个整数向量。现在假设创建了 A 的向量。例如,如果由于 push_back 而发生 A 对象的重新分配(因此向量对象被移动),指向 int 本身的指针是否仍然有效?这个有保证吗?
澄清一下:
class A {
public:
A() {};
std::vector<int> a = {1,2,3,4,5,6,7,8,9};
};
int main()
{
std::vector<A> aVec(2);
int *x1 = &(aVec[1].a[2]);
A *x2 = &aVec[1];
std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";
aVec.resize(30);
int *y1 = &(aVec[1].a[2]);
A *y2 = &aVec[1];
std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";
}
运行这段代码得到:
0x1810088 - 0x1810028 - 2
0x1810088 - 0x18100c8 - 30
所以它表明指针仍然有效。但我想确保这是有保证的,而不仅仅是偶然的事情。我倾向于说这是有保证的,因为向量的内部数据是动态分配的,但是,再次,只是想检查一下。
我看过这里 [Iterator invalidation rules] 但它没有考虑这种特定情况(即矢量对象本身的重新分配)。
更新:
我试过这个来检查我在 Jarod42 的回答的评论中写的内容:
std::vector<std::vector<int>> aVec(2, {1,2,3});
int *x1 = &(aVec[1][2]);
std::vector<int> *x2 = &aVec[1];
std::cout << x1 << " - " << x2 << " - " << aVec.capacity() << "\n";
aVec.resize(30);
int *y1 = &(aVec[1][2]);
std::vector<int> *y2 = &aVec[1];
std::cout << y1 << " - " << y2 << " - " << aVec.capacity() << "\n";
得到这个:
0x16f0098 - 0x16f0048 - 2
0x16f0098 - 0x16f00c8 - 30
这对我来说很奇怪。我期望 x2==y2.
这取决于您 A
的移动构造函数。
但是因为它 (*),它将使用 vector<int>
的移动构造函数作为 a
,并且根据 http://www.cplusplus.com/reference/vector/vector/vector/
[..] no elements are constructed (their ownership is directly transferred).
所以指向整数本身的指针仍然有效。
Edit: (*) A
应该是 noexcept
,std::vector
不保证是 noexcept
.
您的 A
元素将尽可能移动,并且 A
具有隐式移动构造函数和隐式移动赋值运算符,因此成员向量也将被移动。
现在,移动矢量不一定等同于 a.swap(b)
,因此如果您想要 保证,则不能依赖隐式移动函数;你可以自己写。
但是无论您自己保证还是通过查找特定标准库实现的代码获得保证,您都可以确信指向各个元素的指针和迭代器将保持有效:
[C++11: 23.2.1/8]:
The expressiona.swap(b)
, for containersa
andb
of a standard container type other thanarray
, shall exchange the values ofa
andb
without invoking any move, copy, or swap operations on the individual container elements. [..]
遗憾的是,这并不能保证。也就是说,所有 3 个当前实现(libc++、libstdc++ 和 VS-2015)似乎都可以保证这一点。问题是 A
的移动构造函数是否为 noexcept:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
A
的移动构造函数是编译器提供的,因此依赖于 std::vector<int>
的移动构造函数。如果 std::vector<int>
的移动构造函数是 noexcept,那么 A
的移动构造函数是 noexcept,否则不是。
当前草案 N4296 没有将 vector
的移动构造函数标记为 noexcept。但是它允许实现这样做。
这一行:
aVec.resize(30);
如果移动构造函数是 noexcept, 将使用 A
的移动构造函数,否则它将使用 A
的复制构造函数。如果它使用 A
的复制构造函数,则整数的位置将会改变。如果它使用 A
的移动构造函数,整数的位置将保持稳定。
libc++ 和 libstdc++ 将 vector
的移动构造函数标记为 noexcept。因此给 A
一个 noexcept 移动构造函数。
VS-2015 说 A
没有 noexcept 移动构造函数:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
不编译。
尽管如此,VS-2015 并未将整数重新分配到新地址,因此看起来它不符合 C++11 规范。
如果更改 libc++ headers 使得 vector
移动构造函数未标记为 noexcept,则整数确实会重新分配。
委员会最近的讨论表明,每个人都赞成标记 vector
noexcept 的移动构造函数(也许 basic_string
也是,但不是其他容器)。因此,未来的标准可能会保证您寻求的稳定性。与此同时,如果:
static_assert(std::is_nothrow_move_constructible<A>::value, "");
编译,那么你有你的保证,否则你没有。
更新
更新中出现x2 != y2
的原因是这些是vector<vector<int>>
中vector<int>
的地址。这些内部元素必须找到一个新的(更大的)缓冲区来居住,就像内部元素是 int
一样。但与 int
不同的是,内部元素 vector<int>
可以使用移动构造函数移动到那里(int
必须复制)。但是无论是移动还是复制,内部元素的地址都必须改变(从旧的小缓冲区到新的大缓冲区)。此行为与问题的原始部分一致(内部元素也显示为更改地址)。
是的,LWG 2321 is involved, though not a contentious point. In my answer I've already assumed LWG 2321 已经过去了。除了过度急于调试迭代器无缘无故地(和错误地)使自己失效之外,真的没有其他方法可以让事情发生。 Non-debugging 迭代器永远不会失效,指针或引用也不会。
希望我能够轻松创建带有指向缓冲区的箭头的动画。那真的很清楚。我只是不知道如何在可用的时间内轻松地做到这一点。