【问题标题】:Unexpected result when C++ store element into std::vector from return value of function当 C++ 从函数的返回值将元素存储到 std::vector 时出现意外结果
【发布时间】:2018-08-10 20:19:48
【问题描述】:

当函数涉及重新分配时,我发现一些编译器可能会在函数调用之前保存地址。它导致返回值存储在无效地址中。

上面的描述中有一个例子来解释行为。

#include <stdio.h>
#include <vector> 
using namespace std;

vector<int> A; 
int func() { 
    A.push_back(3);
    A.push_back(4);
    return 5; 
} 
int main() { 
    A.reserve(2);
    A.push_back(0);
    A.push_back(1);
    A[1] = func();
    printf("%d\n", A[1]);
    return 0;
}

有一些常见的C++编译器,测试结果如下。

  • GCC(GNU 编译器集合):运行时错误或输出 1
  • Clang:输出5
  • VC++:输出5

这是未定义的行为吗?

【问题讨论】:

  • 至少在 C++ 2014 标准中查看赋值运算符的描述。
  • “当函数涉及重定位时,我发现一些编译器可能会在函数调用之前保存地址” - 这些都不是你认为的意思。这与动态链接无关。 “当函数涉及重新分配”将是准确的。
  • 好点。重新分配以某种方式意味着重新定位(创建新空间,移动东西),但在这里使用重新分配会更清楚,如果这是原作者的真正意思的话。
  • @Useless 他的意思很明显,与链接无关。他的意思是如果向量内容被重新定位(重新分配理论上可以使数据保持不动,在幕后使用 realloc 的库),并且编译器可能已将数据的地址保存在调用周围的寄存器中push_back。我之前遇到过他所说的错误^Wissue,所以你可以说我有偏见,但是......
  • 重读后我知道 OP 是什么意思,但是告诉人们事物的正确名称(或者他们使用的词的实际含义)并没有什么坏处。这些都是有用的信息。

标签: c++ gcc vector compiler-optimization compiler-bug


【解决方案1】:

该行为在 C++17 之前的所有 C++ 版本中未定义。原因很简单,赋值运算符的两侧可以按任意顺序求值:

  • 假设首先评估A[1],您会得到一个int&amp;,此时引用A 的第二个元素。
  • 然后,评估func(),它可以为向量重新分配存储空间,将先前检索到的int&amp; 保留为悬空引用。
  • 最后,执行分配,写入未分配的存储。由于标准分配器缓存内存,操作系统通常不会捕获此错误。

仅在 C++17 中,分配了 special rule 20

在每个简单的赋值表达式 E1=E2 和每个复合 赋值表达式 E1@=E2,每个值的计算和副作用 E2 在每个值计算和副作用之前排序 E1

对于 C++17,A[1] 必须在调用 func() 之后进行评估,然后它会提供定义的、可靠的行为。

【讨论】:

  • 如果使用-std=c++17,使用 GCC 生成的二进制文件不会崩溃。也许 C++17 中的措辞发生了变化?
  • @HolyBlackCat 确实如此。运营商必须尊重关联性
  • 我认为应该改为“哪个可以为向量重新分配存储空间”
  • 可能C++17改变了一些关于求值顺序的规则,可以参考en.cppreference.com/w/cpp/language/eval_order
  • 根据 @MorrisYang 链接的 cppreference 页面上的第 20 项,代码的行为在 C++17 中未定义,因此如果您可以编辑您的答案以显示它是完整的。
【解决方案2】:

如果您检查“迭代器无效”下的documentation,您会看到push_back() 如果改变容量,可能会使每个迭代器无效,因为它必须重新分配内存。请记住,对于std::vector,指针也是有效的迭代器。因为push_back() 可能会或可能不会重新分配,而且您无法知道它是否会重新分配,所以行为是未定义的。

【讨论】:

  • 指针(或引用)和迭代器是分开的,有不同的规则。如果您查看其他容器,您会看到其中一个会失效但另一个不会失效的情况。
  • 当然,但这不是其中一种情况。指向 T 的指针满足 vector::iterator. 的所有要求
  • 仅仅因为指针可以是一个迭代器,并不能使它成为一个迭代器。在std::vector 的情况下,迭代器和引用同时失效,但这并不意味着它们是相同的。这是一种让人迷惑的答案。
  • 如果我编辑我的答案,明确地说“对于std::vector,指针是一个有效的迭代器”,那会令人满意吗?
  • @WillCrawford 我不否认指针是一个完全有效的迭代器实现,vector 的一些早期版本就是这样工作的。但是尝试将指针与v.end() 进行比较,然后告诉我结果如何。
猜你喜欢
  • 2021-09-08
  • 1970-01-01
  • 2015-05-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多