使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?

Posted

技术标签:

【中文标题】使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?【英文标题】:With std::vector, why is &vec[0] undefined behavior, but vec.data() safe?使用 std::vector,为什么 &vec[0] 是未定义的行为,但 vec.data() 是安全的? 【发布时间】:2018-06-16 16:30:57 【问题描述】:

我一直在通过"Link here" 的 isocpp.org 阅读常见问题解答,并注意到std::vector 的警告:

std::vector<int> v;
auto a = &v[0]; // Is undefined behaviour but
auto a = v.data(); // Is safe

来自实际网站:

void g()

  std::vector<Foo> v;
  // ...
  f(v.begin(), v.size());  // Error, not guaranteed to be the same as &v[0]
    ↑↑↑↑↑↑↑↑↑ // Cough, choke, gag; use v.data() instead

此外,如果 std::vectorstd::array 为空,而使用 .data() 始终是安全的 功能。

我不确定我是否完全理解这一点。 ::data() 返回指向数组开头的指针,&amp;[0] 返回开头的地址。我没有看到这里的区别,我不认为 &amp;[0] 正在取消引用任何东西(即,没有读取元素 0 处的内存)。在调试版本中的 Visual Studio 上,访问下标 [0] 会导致断言失败,但在发布模式下它什么也没说。对于默认构造的向量,两种情况下的地址也是 0。

我也不明白关于::begin() 的评论不能保证与::operator[0] 相同。我假设对于向量,begin() 迭代器、::data()&amp;[0] 中的原始指针都是相同的值。

【问题讨论】:

如果它是空的,那么就没有零索引,并且数据数组可能已经或可能没有为向量创建 重点是 v[0],即没有括号,是 UB...,因为您明确要求一个不存在的元素。 @Andre 所以换句话说,你可以指向一个无效的地址,但不能用 &。 @Zebrafish。确切地说,您可以拥有一个地址无效的指针,并且取消引用是 UB,而不是它自己的指针。在 &v[0] 的情况下,您要求的是一个不存在的元素(使用 v[0]),即 UB。然后,您尝试获取它的地址(使用 &),但此时您已经触发了 UB。 @MassimilianoJanes [expr.unary.op]/1 仅定义* 在表达式实际指向对象或函数时的行为。因此,取消引用是遗漏的 UB - 至少在核心问题 232 实际解决之前。 【参考方案1】:

我没有看到这里的区别

&amp;v[0]&amp;(v[0]) 相同,即从v 的第一个元素获取地址。但是当v 为空时根本没有元素,v[0] 只是导致UB,它试图返回一个不存在的元素;试图从中获取地址是没有意义的。

v.data() 总是安全的。它将直接返回指向底层数组的指针。当v 为空时,指针仍然有效(它可能是空指针也可能不是);但请注意,取消引用它(如 *v.data())也会导致 UB,与 v[0] 相同。

另外我不明白关于::begin() 的评论不能保证与::operator[0] 相同

std::vector::begin 将返回一个类型为std::vector::iterator 的迭代器,它必须满足RandomAccessIterator 的要求。它可能是一个原始指针,但不一定是。可以将其作为一个类来实现。

【讨论】:

我一定是对引用和指针有误解,因为我真的以为&返回了地址,它可以分配给指针。 @Zebrafish 是的operator&amp; 确实返回了地址,问题是它试图返回不存在元素的地址。 @songyuanyao 我删除了我的答案,因为它被否决了(我懒得为它辩护:))但我仍然认为你的答案的第一部分不正确;只要不发生左值到纯右值的转换,您可以取消引用空指针;因此,语言中没有任何内容禁止 potential 实现 vector 以使 &v[0] 始终等于 data() ...因此您的解释不完整或不正确 @MassimilianoJanes 我希望你能在你的回答中更多地解释它(使用引用的标准)。 :) 好吧,我检查了标准,它没有定义vector 为空时的行为。这意味着任何行为都是可以接受的。另一方面,标准保证v.data() 始终有效,即使它为空;我认为从标准方面来看,这是这两种情况之间最大的区别。 @songyuanyao 是的,我同意 &v[0] 是 UB,只要 v.empty() 为真。问题是您的解释,这似乎表明它是如此因为您不能“返回不存在元素的地址”。 AFAIK,这不是真的;见[expr.unary:1,2,3]和[expr.add:4,7]【参考方案2】:

为了让您的示例更易于理解,您的问题中缺少的信息是 void f(Foo* array, unsigned numFoos); 在您的 Foo 向量上调用 .begin() 不能保证是一个指针。但有些实现可能表现得像它足以让它工作。

在空向量情况下,v.data() 返回一个指针,但您不知道它指向什么。它可能是一个 nullptr,但不能保证。

【讨论】:

为了说明关于空向量上的v.data() 的声明:参见this test with g++。注意在一种情况下它如何返回空指针,在另一种情况下它返回非空值。【参考方案3】:

这一切都归结为一件简单的事情:您可以为指针添加或减去一个整数值,但尝试取消引用无效指针是未定义的行为。

比如说,

int a[10];
int* p = a;
int* q = p + 10;   // This is fine
int r = *(p + 10)  // This is undefined behaviour

在您的示例中:v[0]*(v's internal pointer+0) 相同,如果向量为空,则会出现问题。

【讨论】:

这是我理解的麻烦,我知道 v[0] 读取 v[0] 处的内存,但认为 &v[0] 只是获取该位置的地址。显然我错了。 v[0] 不仅提供了一个值,还提供了对某个内存位置的引用,比如某种窗口。要做到这一点,它必须适当地解释这个位置内容——如果不能,那就是 UB。指针只关心地址——它不关心某个地址下存储了哪些字节。 @Zebrafish 你没有错,v[0] 读取任何内存,而是取消引用无效指针。取消引用本身不会做任何事情,除非您读取或写入此值,但 C++ 仍然不允许这样做。但它可以允许这样做。从技术上讲,没有什么可以阻止您的代码工作。 只要向量中有元素,&v[0] 就可以工作。我一直使用这种结构,但只有在将矢量调整到我需要的大小之后。调整大小会分配内存,此时 &v[0] 是完全安全的。根据这个讨论,我以后可能会切换到 .data(),但我不会回去更改旧代码...

以上是关于使用 std::vector,为啥 &vec[0] 是未定义的行为,但 vec.data() 是安全的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 std::vector::resize(n, src) 按值传递?

为啥 std::vector max_size() 函数会返回 -1?

为啥使用 STL std::vector 作为 __block 变量会导致内存损坏?

为啥 std::vector::data() 中没有使用指针 typedef?

为啥使用 std::vector 而不是 realloc? [关闭]

为啥不能对 std::vector 使用前向声明?