C++ 可选值<;地图<;字符串,字符串>&燃气轮机;获得;“腐败”;在非常特殊的情况下

C++ 可选值<;地图<;字符串,字符串>&燃气轮机;获得;“腐败”;在非常特殊的情况下,c++,C++,很抱歉这个糟糕的标题,但我看到的是奇怪的,很难简明扼要地解释 基本上,我们的代码中有一个可选的,通过getter/setter访问,有时当我们检查值时,会得到非常奇怪的结果。以下是重现问题的简化代码: #include <optional> #include <map> #include <iostream> using namespace std; optional<map<string, string>> optmap; st

很抱歉这个糟糕的标题,但我看到的是奇怪的,很难简明扼要地解释

基本上,我们的代码中有一个
可选的
,通过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;
        }
}
请注意,在最后一种情况下,这些值只会被“损坏”,在这种情况下,我们使用(auto&item:get().value())进行迭代。更奇怪的是,这种情况似乎只发生在一定长度的字符串上。如果值的长度小于16个字符,则没有问题。如果我将地图更改为包含以下内容:

sample.emplace("fifteencharokay", "15");
sample.emplace("sixteencharweird", "16");
我得到这个输出:

$ ./a.out 
sample:
fifteencharokay, 15
sixteencharweird, 16
result:
fifteencharokay, 15
sixteencharweird, 16
function call:
fifteencharokay, 15
sixteencharweird, 16
inline iteration:
fifteencharokay, 15
harweird, 16
(请注意,
“sixteencharweird”
在最后一行被截断为
“harweird”

这里发生了什么?为什么在这个非常特殊的情况下(长字符串和直接迭代函数调用的结果)会出现问题?是否有某种C++规则,我在这里通过这种方式来打破?

在这个循环中:

for (auto &item : Get().value())
您正在调用未定义的行为,因为由
Get()
返回的临时值将在完整表达式的末尾消失,而循环范围将迭代的
.value()
引用的内存不再存在

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

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

auto const &g = Get();
for (auto &item : g.value())
这是一个例子

事实上,c++20为with initializer construct添加范围正是为了这个目的:

for (auto const &g = Get(); auto &item : g.value())

有趣。实际上,我从你的代码中得到了一个segfault,它运行在Mac上,使用的是Clang的标准库。您使用的是哪种库实现?这是由以下内容修复的:
auto&&x=Get();对于(auto&&item:x.value())
。这似乎是一个终生的问题。
string
的常见实现在内部存储最多16个字符的字符串,在外部存储更长的字符串。我不知道这是否能解释你16个字符的缺陷?@VladFeinstein是的,很好。我将把它添加到我的答案中。我相信使用基于范围的for循环的临时变量是允许的,因为基于范围的for循环会产生一个右值引用并延长其生命周期。它在这里不起作用的原因是被保留的是
value()
的结果,它引用了不存在的
Get()
的结果。如果
Get()
返回的是
map
的副本,而不是
可选的
,则不需要
.value()
,循环应该可以。@FrançoisAndrieux是的,稍微编辑一下,是否更清晰?这看起来不错,但我会留下我的评论作为额外的澄清。既然这不是一种罕见的反模式,有没有办法让编译器对此发出警告?@TEDLYNGOM好吧,它在这种特殊情况下可能没有用处,但一般来说这样做是一种好的做法,所以我将其保留在:)
for (auto const &g = Get(); auto &item : g.value())