【问题标题】:Is there any reason to not use exceptions to test if an element exists in a std::map是否有任何理由不使用异常来测试 std::map 中是否存在元素
【发布时间】:2014-07-20 05:38:52
【问题描述】:

我最近开始在一些项目中使用 c++11,并且还开始大量使用我相对较新的 stl 容器。

我最近编写了一个函数,它的功能与此类似:

CMyClass* CreateAndOrGetClass( int _iObjectId, std::map<int, CMyClass*>& _mapObjectData )
{
    CMyClass* pClassInstance{ nullptr };

    try
    {
        pClassInstance = _mapObjectData.at( _iObjectId);
    }
    catch ( ... )
    {
        pClassInstance = new CMyClass();
        __mapObjectData.insert( _iObjectId, pClassInstance );
    }

    return ( pClassInstance );
}

我的问题是关于在明显不是“异常”的情况下使用异常。这似乎是实现手头目的的一种非常简洁的方式,而不是涉及设置迭代器。

有没有我可能遗漏的关于将异常用于此类目的的任何陷阱?


测量*

因此,为了跟进,我做了一些性能测试,将基于异常的代码与所选答案中的代码示例进行比较。这是一个很好的练习,可以通过使用正常代码流的异常来验证性能不佳的建议。它还提供了一些很好的参考资料,用于断言在不引发异常时接近零性能损失。

虽然这些测试并不详尽,但以下是我观察到的:

每个测试都使用 rand() 作为映射键的源(生成最多 32768 个元素)对大量插入/提取运行:

异常方法:平均 5.99 秒
查找/添加方法:平均 0.75 秒

将随机元素的范围扩大十倍会返回这些数字:

异常方法:平均 56.7 秒
查找/添加方法:平均 4.54 秒

然后我用所有可能的键条目预先填充了地图,这样尝试就不会抛出:

异常方法:平均 0.162 秒
查找/添加方法:平均 0.158 秒

奇怪的是,使用 MS VStudio,调试模式代码使用异常处理方法更快。

【问题讨论】:

  • 你的代码会更慢,更难调试,当然也更难读。

标签: c++ exception c++11 map stl


【解决方案1】:

异常是一种将故障处理与正常案例代码完全分开的方法。

在您的代码中,它们用于正常情况,这违背了目的并且没有任何优势。

在当前的特定情况下,使用[] 索引,如果键不存在,它会自动插入键。并且更普遍地将条件构造用于简单的条件控制流。 一些例外情况,在这些情况下,异常对于表达正常的情况控制流是有意义的(例如,从深度嵌套的递归调用中返回结果),因为代码可以变得更简单和更清晰,但是这些例外情况是……例外。


关于效率,抛出异常的成本很高,因为 C++ 编译器经过优化,仅将异常用于故障处理,这被认为是罕见

当失败成为常态时,重新考虑失败的工作定义。

但是,仅仅有可能引发异常的开销很小,低至 0。因此,您不应该害怕使用异常来安全地处理故障,并且不会让人分心地远离正常的案例代码。使用得当,把代码分为正常情况和失败,异常是双赢的。


a comment 回复您评论的另一个答案,

虽然我喜欢这种简洁性,但如果无法实例化该类,它可能会给地图留下 nullptr。

是一种失败的情况,使用异常处理是正确的方法。

例如,

Your_class* creative_at( int const id, std::map<int, YourClass*>& object_data )
{
    // Basic non-creating at:
    {
        auto const it = object_data.find( id );
        if( it != object_data.end() ) { return it->second; }
    }

    // Create:
    std::unique_ptr<Your_class> p( new Your_class() );    // May throw.
    object_data[id] = p.get();                            // May throw.
    return p.release();
}

此代码是基于异常的,但看不到try-catch

与手动try-catch-finally 的Java 方法不同,在C++ 中主要让析构函数进行自动清理,例如本例中的std::unique_ptr 析构函数;这种方法称为RAII,是Resource Acquisition Is Initialization的缩写。

【讨论】:

  • 这是一个很好的例子。我喜欢使用 unique_ptr 来控制创建和分配过程中可能出现的异常。
  • FWIW 我对 c++ 相当熟悉,并且已经使用了很长时间,最近阅读了 Bjarne 的书的第 4 版以了解最新的技术。正是从那本书中,我真正接受了测量的持续建议,这个线程不应该没有。原来我最初发布的异常方法比我系统上的这个方法慢了大约 80 倍(在 Win7 上,32 位发布版本)。
【解决方案2】:

编译器通常使用一种策略来实现运行时开销为零的异常,只要没有抛出任何异常,但如果抛出异常,由于必须展开堆栈的异常处理机制,它会影响程序的性能。

即使这对您的用例来说是可以接受的,但在您的用例中使用异常来管理控制流也没有任何好处。使用map::find不仅更加简洁,而且更加地道。

auto iter = _mapObjectData.find(_iObjectId);
if(iter == _mapObjectData.end()) {
  auto instance = new CMyClass();
  _mapObjectData.insert(std::make_pair(_iObjectId, instance));
  return instance;
}
return iter->second;

@Mehrdad 在 cmets 中有一个 good suggestion 以使用 map::lower_bound 而不是 map::find 来定位密钥。这样做的好处是,如果键不存在,则返回值可以用作map::insert提示,这应该会带来更好的插入性能。

auto iter = _mapObjectData.lower_bound(_iObjectId);
if(iter == _mapObjectData.end() || iter->first != _iObjectId) {
  auto instance = new CMyClass();
  _mapObjectData.insert(iter, std::make_pair(_iObjectId, instance));
  return instance;
}
return iter->second;

我也强烈建议您更改地图的类型

std::map<int, CMyClass*>

std::map<int, std::unique_ptr<CMyClass>>

将拥有资源的原始指针粘贴到标准库容器中通常比它的价值更麻烦。

【讨论】:

  • @T.C.谢谢你。不编译就发布代码的危险:)
  • 谢谢。有趣的是,该代码看起来与我刚刚写的非异常版本完全一样,因为我想测量是否有任何差异。我更关心的是你所说的,以及我使用异常进行流量控制而不是他们陈述的原因的问题的一部分。
  • @Praetorian:您应该使用lower_bound 而不是find(但不要忘记再次检查密钥以确保它是正确的密钥)。此外,我认为_mapObjectData.insert(_iObjectId, instance) 甚至不起作用:-) 你应该将它传递给pair(可能连同一个迭代器一起避免第二次查找,希望你能从lower_bound 得到一个) .
  • 如果insert 抛出内存泄漏。
  • @Mehrdad 感谢您发现 map::insert 错误,我不假思索地复制了 OP 的代码。我在答案中添加了lower_bound 建议。
【解决方案3】:

C++ 中避免使用过多异常是有原因的:异常处理繁重(因为所有中间调用帧中的局部变量的所有析构函数都必须执行)。

Ocaml 中,异常处理是轻量级的,因此更有可能像您建议的那样使用它们。

在您的示例中,我宁愿使用std::mapfind 成员函数

【讨论】:

  • 感谢您的回复,但是我真的在寻找专门针对这种异常包含在测试范围内的情况的建议。一般来说,我一直在避免异常,因为在抛出异常时语言没有强制执行来捕获它们。
【解决方案4】:

是的,因为传播异常和你的代码本身都不是特别有效。

忽略您不应该首先存储原始指针的事实,下一个最正确的和最有效的(并且可读!)方式来做你正在做的事情使用operator[]:

CMyClass* CreateAndOrGetClass(int _iObjectId, std::map<int, CMyClass*>& _mapObjectData)
{
    CMyClass* &pClassInstance = _mapObjectData[_iObjectId];
    if (!pClassInstance) { pClassInstance = new CMyClass(); }
    return pClassInstance;
}

这样,在地图中的查找只进行一次,并且就地构造值。

如果当然,实际上您可能应该只存储 CMyClass 作为值,而不是 CMyClass *,但这与我在这里的回答无关。

【讨论】:

  • 这将对映射中已经存在的空指针执行new(OP 的代码没有,尽管尚不清楚这是否是故意的)
  • @MattMcNabb:很好,但我不希望 OP 预期的空值是地图中的有效值。
  • 虽然我喜欢这种简洁性,但如果无法实例化该类,它可能会在地图上留下一个 nullptr。 @Matt,我特别不希望地图中出现 nullptrs ,这将被捆绑在新失败引发的异常中。 FWIW,原始指针的使用是经过深思熟虑和理解的,因此无需将其添加为任何答案的一部分。
  • @jgibbs:不同的程序是否也在访问地图?因为在我看来,这个过程似乎是唯一访问地图的过程,在这种情况下,您不必担心 null 存在,因为它不会对任何事情产生不利影响。
  • @jgibbs:此外,我什至不理解你的担忧:如果你担心类没有被正确实例化,那么这意味着你希望你的代码是异常安全的(异常是代码可以在中间中止并在地图中留下悬空的空值的唯一方法)。但是,如果您担心抛出异常,那么您的代码已经存在很多更大 问题(您似乎已经意识到的原始指针),而您完全忽略了这些问题。与您忽略的泄漏相比,地图中悬空的空值将是您最不关心的问题。
【解决方案5】:

正如其他人所指出的,异常很慢然后被抛出/捕获。

但这并不意味着替代品一定很丑。

if(!mymap.count(objectId))
{

}

告诉您您的对象是否丢失。

另外,你确定你需要地图值的指针吗?这样做的唯一原因是如果您的类的复制构造函数非常慢,或者该类不可复制/可移动。即使在这种情况下,您也需要使用 unique-ptrshared-ptr

此外,带有 C、下划线 i、p 等前缀的匈牙利表示法版本越来越不受欢迎。许多人认识到这比帮助更麻烦。请参阅 Alexandescu & Sutter 的 C++ 编码标准,第 0 章。

【讨论】:

  • 我只对探索使用异常处理的开销和问题感兴趣。我理解/反对使用原始指针的原因,如果我把这些放在我的例子中,它不会改变我正在寻找答案的问题,但无论如何感谢你的建议。匈牙利符号方法的一些人工制品仍然非常有用,我们选择使用其中一些来造福我们所有的开发人员。我个人从来没有发现使用有用位的问题。
猜你喜欢
  • 1970-01-01
  • 2020-08-27
  • 2013-11-11
  • 1970-01-01
  • 1970-01-01
  • 2021-03-04
  • 2011-12-20
  • 2017-07-12
  • 1970-01-01
相关资源
最近更新 更多