繁体   English   中英

是否有任何理由不使用异常来测试std :: map中是否存在元素

[英]Is there any reason to not use exceptions to test if an element exists in a std::map

我最近开始在许多项目中使用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,异常处理方法的调试模式代码更快。

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

即使这对于您的用例是可接受的,在您的情况下使用异常来管理控制流也没有任何优势。 使用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在评论中有一个很好的建议 ,使用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>>

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

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

在你的代码中,它们被用于正常情况,这会破坏目的而没有任何优势。

在特定情况下,使用[]索引,如果密钥已经存在则会自动插入密钥。 更常见的是使用条件结构来实现简单的条件控制流程。 一些特殊情况下的例外是有意义表达正常的情况下,控制流(如返回从一个深度嵌套的递归调用的结果),在这个意义上的代码可以变得更简单,更清晰,但这些例外的情况是例外...。


关于效率,抛出异常是非常昂贵的,因为C ++编译器针对故障处理使用异常进行了优化,这被认为是罕见的

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

但是,只是抛出异常的可能性很小,低至0,开销。 因此,您不应该害怕使用异常来安全地处理失败,并且不会分散注意力,使其远离正常的案例代码。 正确使用,将这种代码划分为正常情况和失败,例外是双赢的。


评论你回答的另一个答案时,

虽然我喜欢这种简洁,但如果无法实例化类,它可能会使地图留下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 ,是资源获取初始化的缩写。

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

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

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

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

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

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

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

如果当然,实际上你应该只将CMyClass存储为值,而不是CMyClass * ,但这与我的答案无关。

正如其他人所指出的那样,异常是缓慢的然后抛出/捕获。

但这并不意味着替代方案必须是丑陋的。

if(!mymap.count(objectId))
{

}

告诉您对象是否丢失。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM