将自身大小插入到std::map中

3

我刚刚遇到了代码(C++14)中的一个奇怪的错误,原因是 std::map 的行为出乎了我的意料。以下是一个简单的示例来演示这种行为:

#include <iostream>
#include <map>

int main()
{
    std::map<int, int> m;
    for(int i = 0; i < 3; ++i) {
        m[m.size()] = m.size();
    }
    for(const std::pair<int, int>& e : m) {
        std::cout << e.first << " " << e.second << std::endl;
    }
    
    return 0;
}

这将打印:

0 1                                                                                                                                                                            
1 2                                                                                                                                                                            
2 3

我原本期望的是:
0 0
1 1
2 2

这是怎么回事?这个地图首先添加一个元素,并将 first 设置,只有当地图的大小已经增加时才设置 second?我不太明白这样做的意义在哪里。或者还有其他解释吗?谢谢!


1
这不就是“i++ + ++i”问题吗? - IWonderWhatThisAPIDoes
1
你使用的是哪个版本的C++?这很重要。 - NathanOliver
@NathanOliver,我观察到这个问题是使用C++14(已添加到我的问题中)。我没有预料到这会有影响。 - Petri Hirvonen
1
C++17引入了关于操作顺序的新规则。在此之前,赋值语句的任一侧都可以先进行评估。自那时以来,必须先评估右侧。请参见https://en.cppreference.com/w/cpp/language/eval_order规则20。 - François Andrieux
@FrançoisAndrieux,这对我来说更有意义。可惜NetBeans不支持C++17。 - Petri Hirvonen
2个回答

11

在这个表达式中发生了一些事情

m[m.size()] = m.size();

首先,需要评估m[m.size()]= m.size()。在C++14中,这个评估顺序是不确定的。可能会先评估m[m.size()],也可能会先评估= m.size()。如果先评估m[m.size()],那么你会看到你收到的结果。如果先评估= m.size(),那么你就会得到你期望的输出。

如果你想要

0 0
1 1
2 2

然后你需要自己保证那个顺序。你可以使用 map::insert() 来做到这一点:

m.insert(m.size(), m.size());

从C++17开始,情况发生了改变。 标准被修改为:

赋值运算符(=)和复合赋值运算符的结合方向均为从右至左。所有运算符都需要一个可修改的左值作为其左操作数;它们的结果是指向左操作数的左值。如果左操作数是位域,则结果总是位域。在所有情况下,赋值在右操作数和左操作数的值计算之后顺序执行,并在赋值表达式的值计算之前执行。右操作数在左操作数之前顺序执行。相对于不确定顺序的函数调用,复合赋值的操作是单个评估。

现在保证= m.size()先于m[m.size()]执行,并且你会得到你期望的顺序。


好的,那么正如我所怀疑的那样 - 那里没有更多奇怪的事情发生。你是说在C++17之前评估哪一边是“随机”的吗?还是由于某些复杂的规则不明显?或者这取决于编译器的优化? - Petri Hirvonen
@PetriHirvonen 这取决于编译器决定先评估哪一侧。 - Remy Lebeau
@PetriHirvonen 在C++14中是“随机”的。一些实现会先评估左侧,而另一些则会先评估右侧。 - NathanOliver
Cppreference将C++17之前的求值顺序称为“不确定顺序”。我认为这与实现定义不同(后者要求编译器记录其关于此行为的行为)。 - HolyBlackCat
1
@HolyBlackCat 我已更新答案,使用不确定顺序。我以为它是实现定义的,但看起来我记错了那个单词。 - NathanOliver

5

让我们更详细地书写循环代码:

for(int i = 0; i < 3; ++i) {
    int& temp = m.operator[](m.size());
    temp = m.size();
}

请注意,调用 operator[] 的时候,已经执行了一个默认初始化元素的插入操作,然后再为它赋值。因此,在赋值操作右侧被执行之前,map 已经增长了。
为了解决这个问题,请确保在使用 operator[] 之前确定大小。
for(int i = 0; i < 3; ++i) {
    size_t v = m.size();
    m[v] = v;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接