optional<map<string, string>> 中的值在非常特殊的情况下变得“损坏”

wly*_*les 4 c++

对不起,标题很差,但我所看到的很奇怪,很难简洁地解释。

基本上,我们optional<map<string, string>>的代码中有一个通过 getter/setter 访问的,有时当我们检查值时,我们会得到非常奇怪的结果。这是重现问题的简化代码:

#include <optional>
#include <map>
#include <iostream>

using namespace std;

optional<map<string, string>> optmap;

static void Set(optional<map<string, string>> m);
static optional<map<string, string>> Get();
static void PrintMap(map<string, string> m);

int main(int const argc, char const * const *argv)
{
        map<string, string> sample;
        sample.emplace("testtesttesttest1", "testtesttesttest1");
        sample.emplace("testtesttesttest2", "testtesttesttest2");
        sample.emplace("testtesttesttest3", "testtesttesttest3");

        cout << "sample:" << endl;
        PrintMap(sample);

        Set(sample);
        map<string, string> result = Get().value();

        cout << "result:" << endl;
        PrintMap(result);

        cout << "function call:" << endl;
        PrintMap(Get().value());

        cout << "inline iteration:" << endl;
        for (auto &item : Get().value())
        {
                cout << item.first << ", " << item.second << endl;
        }
}

static void Set(optional<map<string, string>> m)
{
        optmap = m;
}

static optional<map<string, string>> Get()
{
        return optmap;
}

static void PrintMap(map<string, string> m)
{
        for (auto &item : m)
        {
                cout << item.first << ", " << item.second << endl;
        }
}
Run Code Online (Sandbox Code Playgroud)

我编译使用g++ -std=c++17并在我最近的运行中得到了这个输出:

$ ./a.out 
sample:
testtesttesttest1, testtesttesttest1
testtesttesttest2, testtesttesttest2
testtesttesttest3, testtesttesttest3
result:
testtesttesttest1, testtesttesttest1
testtesttesttest2, testtesttesttest2
testtesttesttest3, testtesttesttest3
function call:
testtesttesttest1, testtesttesttest1
testtesttesttest2, testtesttesttest2
testtesttesttest3, testtesttesttest3
inline iteration:
@?M?OVtesttest1, ??M?OVtesttest1
??M?OVtesttest2, ??M?OVtesttest2
??M?OVtesttest3, testtest3
Run Code Online (Sandbox Code Playgroud)

请注意,在我们使用for (auto &item : Get().value()). 更奇怪的是,这似乎只发生在一定长度的字符串上。如果值的长度小于 16 个字符,则没有问题。如果我将地图更改为包含以下内容:

sample.emplace("fifteencharokay", "15");
sample.emplace("sixteencharweird", "16");
Run Code Online (Sandbox Code Playgroud)

我得到这个输出:

$ ./a.out 
sample:
fifteencharokay, 15
sixteencharweird, 16
result:
fifteencharokay, 15
sixteencharweird, 16
function call:
fifteencharokay, 15
sixteencharweird, 16
inline iteration:
fifteencharokay, 15
harweird, 16
Run Code Online (Sandbox Code Playgroud)

(注意"sixteencharweird""harweird"在最后一行被截断)

这里发生了什么?为什么我们在这种非常特殊的情况下会遇到问题(长字符串并直接迭代函数调用的结果)?我通过这种方式迭代是否违反了某种 C++ 规则?

cig*_*ien 8

在这个循环中:

for (auto &item : Get().value())
Run Code Online (Sandbox Code Playgroud)

您正在调用未定义的行为,因为返回的临时值Get()将在完整表达式结束时消失,并且.value()您的范围 for 循环将迭代的引用不再存在的内存。

您注意到长度小于 16 个字符的字符串的奇怪行为可能是由于小字符串优化造成的。由于字符串保留在短字符串的内部缓冲区中,您仍然可以看到那里的内存。当然,这仍然是UB,你不能依赖它。

您可以通过执行以下操作来解决此问题:

auto const &g = Get();
for (auto &item : g.value())
Run Code Online (Sandbox Code Playgroud)

这是一个演示

事实上,c++20 正是为了这个目的添加了 range-for with initializer 构造:

for (auto const &g = Get(); auto &item : g.value())
Run Code Online (Sandbox Code Playgroud)

  • 我相信使用基于范围的 for 循环的临时变量是允许的,因为基于范围的 for 循环会生成右值引用并延长其生命周期。它在这里不起作用的原因是,保留的是“value()”的结果,它引用了“Get()”的结果,但它不存在。如果“Get()”返回“map&lt;string, string&gt;”的副本而不是“可选”,则不需要“.value()”并且循环应该没问题。 (2认同)