【问题标题】:stl map operator[] bad?stl map operator[] 不好?
【发布时间】:2012-02-06 17:12:15
【问题描述】:

我的代码审阅者指出,map 的 operator[] 的使用非常糟糕,会导致错误:

map[i] = new someClass;    // potential dangling pointer when executed twice

或者

if (map[i]==NULL) ...      // implicitly create the entry i in the map 

虽然我在阅读了insert()更好的API后理解了风险,因为它检查重复,因此可以避免发生悬空指针,我不明白如果处理得当,为什么[]可以根本不用?

我选择地图作为我的内部容器正是因为我想使用它快速且不言自明的索引功能。

我希望有人可以和我多争论或者站在我这边:)

【问题讨论】:

  • 一般来说,“永远不要这样做”的规则是不好的。在某些情况下,您确实需要某些东西并且您应该打破规则。
  • (在相关说明中,请参阅Boost Pointer Containers 以了解将保留指针并在需要时销毁它们的容器)
  • 你应该避免使用 [] 而更喜欢插入/查找。这是常识,比如避免未初始化的变量、编译器警告等。
  • 在这种情况下我看不到任何悬空指针。可能存在内存泄漏,但这不一样!悬空指针是指向不再存在的东西的指针,但看不到可能导致这种情况的单个 delete(或任何其他原因)。
  • 我希望审阅者也指出使用原始指针管理内存非常糟糕(尤其是当您将它们放入复杂的容器中时);否则,您将调试大量内存泄漏。

标签: c++ stl map operators


【解决方案1】:

如果你的地图是这样的:

std::map< int, int* >

然后你输了,因为下一个代码 sn-p 会泄漏内存:

std::map< int, int* > m;
m[3] = new int( 5 );
m[3] = new int( 2 );

如果处理得当,为什么[]根本不能用?

如果您正确地测试了您的代码,那么您的代码应该仍然无法通过代码审查,因为您使用了原始指针。

除此之外,如果使用得当,使用map::operator[] 没有任何问题。但是,使用插入/查找方法可能会更好,因为可能会进行静默地图修改。

【讨论】:

  • 在您的示例中没有特定于 op[] 的内容。就好像你覆盖了一个指针。这不是mapop[]是“坏”的原因。
【解决方案2】:
  1. operator [] 被避免插入,因为同样的原因 你在你的问题中提到。它不检查重复键 并覆盖现有的。
  2. operator []std::map 中的搜索大多避免使用。 因为,如果您的map 中不存在密钥,那么operator []静默创建新密钥并初始化它(通常是 0)。这可能不是在所有情况下都是可取的。一个应该使用 [] 仅在需要创建密钥时,如果它不存在。

【讨论】:

  • 那么我的问题是,我们什么时候应该真正使用运算符呢?一般来说,索引不是映射的最大特征之一吗?
  • 另一个问题:如何测试某个索引的条目是否为空?而当我们一步步来的时候,如果 (map[i]==NULL) 还不错,如果对中的第二个是指针,它会被初始化为零对吗?
  • @Figo,在查找元素时,如果未找到键,则返回 end() 迭代器。如果在您的应用程序中,如果找不到密钥,则需要创建密钥,然后使用operator []
  • 实际上,map 的最大特点通常是键值映射,通常实现为某种搜索树,并保证查找操作的复杂度为 O(log(n)) ;) .除此之外,您还可以使用 operator[] (其性能应该比 insert 或 find 本身的性能稍差,因为它同时完成了这两项工作)您知道自己在做什么和 i> 你可以保证你的假设不会在代码维护下被打破(你通常不能)。
【解决方案3】:

你说得对,map::operator[] 必须小心使用,但它可能非常有用:如果你想在地图中找到一个元素,如果没有,则创建它:

someClass *&obj = map[x];
if (!obj)
    obj = new someClass;
obj->doThings();

地图中只有一个查找。 如果new 失败,您当然可以从映射中删除 NULL 指针:

someClass *&obj = map[x];
if (!obj)
    try
    {
        obj = new someClass;
    }
    catch (...)
    {
        obj.erase(x);
        throw;
    }
obj->doThings();

当然,如果你想找到一些东西,而不是插入它:

std::map<int, someClass*>::iterator it = map.find(x); //or ::const_iterator
if (it != map.end())
{
    someClass *obj = it->second;
    obj->doThings();
}

【讨论】:

  • 我认为 try/catch 表明这是一个坏主意...您可以使用 find/insert 更简单。
  • 是的,但是 find / insert 会搜索该项目两次。 operator[] 只需一次访问即可完成!
  • 确实如此,如果这是一个问题,您应该使用 lower_bound/insert。请参阅我的更新答案。
【解决方案4】:

诸如“使用地图的 operator[] 非常糟糕”之类的声明应该始终是几乎宗教信仰的警告信号。但与大多数此类说法一样,某处潜伏着一些真相。然而,这里的事实与 C++ 标准库中的几乎所有其他构造一样:小心并知道自己在做什么。您可以(意外地)滥用几乎所有内容。

一个常见问题是潜在的内存泄漏(假设您的地图拥有这些对象):

std::map<int,T*> m;
m[3] = new T;
...
m[3] = new T;

这显然会泄漏内存,因为它会覆盖指针。在这里正确使用 insert 也不是一件容易的事,而且很多人犯了一个无论如何都会泄漏的错误,比如:

std::map<int,T*> m;
minsert(std::make_pair(3,new T));
...
m.insert(std::make_pair(3,new T));

虽然这不会覆盖旧指针,但它不会插入新指针,也会泄漏它。插入的正确方法是(可能使用智能指针更好地增强):

std::map<int,T*> m;
m.insert(std::make_pair(3,new T));
....
T* tmp = new T;
if( !m.insert(std::make_pair(3,tmp)) )
{
    delete tmp;
}

但这也有点难看。我个人更喜欢这种简单的情况:

std::map<int,T*> m;

T*& tp = m[3];
if( !tp )
{
    tp = new T;
}

但这可能与您的代码审查员不允许使用 op[] 的个人偏好相同...

【讨论】:

  • 如果new T 抛出怎么办?然后你在地图中有一个“空”条目。
  • @ronag:这就是我谈论智能指针的原因。这并不是一个完美的例子,而是为了转移这个想法,因为无论如何它都必须适应这种情况。
  • 我仍然鼓励你写一个“正确”的例子。如果new 抛出,使用智能指针将如何改变您在地图中将有一个空条目的事实。
  • @ronag:不会。我只是指出所有这一切都不是一个完美的例子。在一个完美的例子中,地图中会有智能指针,或者类似的结构,因为当地图超出范围时,内存也会泄漏等等。我个人认为我们应该把它留在基础上,以表明这一点,并且不要把事情复杂化。我个人什至在一个你通常不关心新投掷的环境中工作,所以这是否有助于 OP 解决他的真正问题只能由他来判断,我认为他会通过阅读这些 cmets 来管理它。
  • 我倾向于不同意,因为您展示的示例不正确,快速阅读者可能会认为它是正确的并开始使用它。
【解决方案5】:

operator[] 唯一有用的时候(我能想到的)是当你想设置一个键的值(如果它已经有值就覆盖它),并且你知道它是 覆盖安全(应该是这样,因为您应该使用智能指针,而不是原始指针)并且 默认构造便宜,并且在某些情况下,该值应该具有 无抛出构造和分配

例如(类似于你的第一个例子)

std::map<int, std::unique_ptr<int>> m;
m[3] = std::unique_ptr<int>(new int(5));
m[3] = std::unique_ptr<int>(new int(3)); // No, it should be 3.

否则,根据上下文有几种方法可以做到这一点,但我建议始终使用通用解决方案(这样你就不会弄错)。

找到一个值,如果它不存在就创建它:

1.通用解决方案(推荐,因为它始终有效)

std::map<int, std::unique_ptr<int>> m;
auto it = m.lower_bound(3);
if(it == std::end(m) || m.key_comp()(3, it->first))
   it = m.insert(it, std::make_pair(3, std::unique_ptr<int>(new int(3)));

2。具有廉价的默认价值构造

std::map<int, std::unique_ptr<int>> m;
auto& obj = m[3]; // value is default constructed if it doesn't exists.
if(!obj)
{
   try
   {
      obj = std::unique_ptr<int>(new int(3)); // default constructed value is overwritten.
   }
   catch(...)
   {
      m.erase(3);
      throw;
   }
}

3.具有廉价的默认构造和不抛出值的插入

std::map<int, my_objecct> m;
auto& obj = m[3]; // value is default constructed if it doesn't exists.
if(!obj)
   obj = my_objecct(3);

注意:您可以轻松地将通用解决方案包装到辅助方法中:

template<typename T, typename F>
typename T::iterator find_or_create(T& m, const typename T::key_type& key, const F& factory)
{
    auto it = m.lower_bound(key);
    if(it == std::end(m) || m.key_comp()(key, it->first))
       it = m.insert(it, std::make_pair(key, factory()));
    return it;
}

int main()
{
   std::map<int, std::unique_ptr<int>> m;
   auto it = find_or_create(m, 3, []
   {
        return std::unique_ptr<int>(new int(3));
   });
   return 0;
}

请注意,我为创建案例传递了一个模板化工厂方法而不是一个值,这样在找到值并且不需要创建值时就没有开销。由于 lambda 作为模板参数传递,编译器可以选择内联它。

【讨论】:

  • 我认为你在第一句话中漏掉了一个词
  • @LightnessRacesinOrbit:确实。
  • 好吧,简单是旁观者的眼中。如果新元素的插入不能抛出(比如std::map&lt;int, int&gt;,则不需要try/catch,默认构造将是微不足道的,所以我的解决方案将是......完美!
  • 好吧,new 总是可以抛出,你仍然会创建一个不必要的默认构造值。但是,是的,在某些情况下(没有抛出保证和廉价的默认构造)您的解决方案更简单。
  • 作为一个刚接触 C++ 的人,我觉得“通用”解决方案 (1) 需要 3 行代码而要避免的是一行代码,这有点疯狂。您建议将通用解决方案包装在辅助方法中,这很好,但如果这是我们大部分时间应该使用的,那么为什么地图的作者没有为我们这样做呢?它会让成千上万的程序员不必输入自己的辅助方法,对吗?
【解决方案6】:
 map[i] = new someClass;    // potential dangling pointer when executed twice

这里,问题不在于地图的operator[],而是*缺少智能指针。 您的指针应该存储到某个 RAII 对象(例如智能指针)中,该对象会立即获得分配对象的所有权,并确保它会被释放。

如果您的代码审阅者忽略了这一点,而是说您应该热衷于 operator[],请给他们买一本好的 C++ 教科书。

if (map[i]==NULL) ...      // implicitly create the entry i in the map 

确实如此。但那是因为operator[] 被设计成不同的行为。显然,你不应该在它做错事的情况下使用它。

【讨论】:

  • 我看不出智能指针在哪里可以解决一般问题(在这种情况下,当值语义更合适时,可能会使用指针开始)。在少数情况下智能指针是合适的,但我还没有看到映射到智能指针是正确解决方案的情况。
  • @JamesKanze:但他们会解决“如果我调用new 并将结果存储到同一个目标对象中,我会泄漏内存”的具体问题,这似乎是假设的问题在第一种情况下,与std::map 关系不大。我是否误解了 OP?
  • 这种情况下的具体问题很可能不是map[]的使用,而是new的使用。如果您使用值语义,而不是 new 和指针,那么无论发生什么,您都不会发生泄漏。 (另一个可能的具体问题是代码不希望替换现有值。在这种情况下,当然应该使用insert,而不是[],并测试返回值。)跨度>
【解决方案7】:

通常的问题是 operator[] 隐式创建一个与传入的键关联的值,如果该键尚未出现,则在映射中插入一个新对。从那时起,这可能会破坏您的逻辑,例如当你搜索某个键是否存在时。

map<int, int> m;
if (m[4] != 0) {
  cout << "The object exists" << endl; //furthermore this is not even correct 0 is totally valid value
} else {
  cout << "The object does not exist" << endl;
}
if (m.find(4) != m.end()) {
  cout << "The object exists" << endl; // We always happen to be in this case because m[4] creates the element
}

我建议仅当您知道您将引用地图中已经存在的键时才使用 operator[](顺便说一句,这种情况并不常见)。

【讨论】:

  • 但是你怎么能确定密钥不存在呢?我知道operator[] 的唯一正确用法是当您想要一个新的默认构造对象时,如果该对象尚不存在(例如符号映射,您在源中看到的每个符号都应该出现在映射中)。
  • 如果键存在,则不会遇到 - 返回现有值。当它不存在时会出现问题 - 构造了一个新对象。您知道存在对象的示例是您的键来自某种枚举。
  • 什么时候使用枚举作为std::map 的键类型?如果键类型是枚举,我会使用std::vector,甚至是C 风格的数组。我不知道在代码的整个生命周期中我可以确定密钥已经存在的任何情况。 (即使使用枚举,也可以为枚举添加一个值,但忘记修改映射的初始化以包含它。)
  • 我在更广泛的意义上使用了枚举。我可以给你举个例子:想象你开发的网站由几个屏幕组成,一个允许添加新的,比如联系人,另一个允许你选择其中的一些。在后者中,您的软件按字母顺序列出所有联系人(因此您需要有序结构),因此您知道选择已经存在于地图中,您可以使用 operator[] 查询名称。
  • 但是你怎么知道你拥有的字符串是有效字符串之一呢?问题是,您可以知道映射中是否包含潜在键的唯一方法是使用 find。 (但我想我明白了你的意思:你已经遍历了地图以创建其键的列表,并且正在使用从列表中选择的元素。我仍然倾向于使用带有断言的 find如果它返回结束指针,但这只是防御性编程,可能不合理。)
【解决方案8】:

[] 根本不是问题。在容器中存储原始指针是个问题。

【讨论】:

  • 容器本身的原始指针不是问题。当值语义更合适时,这(可能)是使用指针的问题。
  • @James:我不同意。共享指针value_type 在这里可能有意义。
  • 极不可能。关于我能想到的唯一情况是对象是否是多态的。即便如此:在我见过的情况下,对象要么具有静态生命周期,要么被插入到构造函数中并在析构函数中删除;在这两种情况下,shared_ptr 会导致问题,而不是解决问题。
  • @James:或者您正在创建一些索引和对象之间的映射,这些对象也“包含”在其他地方。经常发生,并且共享指针最终是为此目的而设计的(或者,至少,容器的使用只是其目的的大规模扩展)。
  • 如果对象包含在其他地方,shared_ptr 将导致未定义的行为。
【解决方案9】:

地图的operator[] 本身并没有错,只要它 语义对应于你想要的。问题是定义你的 想要(并且知道operator[] 的确切语义)。有的时候 当隐式创建具有默认值的新条目时 不存在正是您想要的(例如,计算文本中的单词 文档,其中++ countMap[word] 就是您所需要的);有 很多时候它不是。

您的代码中更严重的问题可能是您正在存储指针 在地图中。更自然的解决方案可能是使用map <keyType, someClass>,而不是map &lt;keyType, SomeClass*&gt;。但同样,这 取决于所需的语义;比如我用了很多map 在程序启动时初始化一次,并带有指向静态的指针 实例。如果您是 map[i] = ... 处于初始化循环中, 在启动时执行一次,可能没有问题。如果是什么 在代码的许多不同地方执行,可能有一个 问题。

解决问题的方法不是禁止operator[](或映射到 指针)。解决方案是从指定确切的语义开始 你需要。如果std::map 不直接提供它们(它很少 确实),编写一个小的包装类,它定义了你的确切语义 想要,使用std::map 来实现它们。因此,你的包装器 operator[] 可能是:

MappedType MyMap::operator[]( KeyType const& key ) const
{
    MyMap::Impl::const_iterator elem = myImpl.find( key );
    if ( elem == myImpl.end() )
        throw EntryNotFoundError();
    return elem->second;
}

或:

MappedType* MyMap::operator[]( KeyType const& key ) const
{
    MyMap::Impl::const_iterator elem = myImpl.find( key );
    return elem == myImpl.end()
        ?   NULL   //  or the address of some default value
        :   &elem->second;
}

同样,您可能希望使用 insert 而不是 operator[] 你真的想插入一个不存在的值。

而且我几乎从未见过你会立即插入 newed 对象到地图中。使用new 的常见原因和 delete 是有问题的对象有一些特定的生命周期 他们自己的(并且不可复制——尽管不是绝对规则,如果 你是newing 一个支持复制和赋值的对象,你是 可能做错了什么)。当映射类型是指针时, 那么要么指向的对象是静态的(并且地图更多或 初始化后的常数较小),或者插入和删除是 在类的构造函数和析构函数中完成。 (但这只是 一般规则;当然也有例外。)

【讨论】:

    猜你喜欢
    • 2017-02-27
    • 2022-08-24
    • 2018-01-01
    • 2016-08-30
    • 1970-01-01
    • 2012-09-04
    • 2017-03-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多