布尔测试与字符串流中的异常

问题描述 投票:0回答:1

std::istringstream
/
std::ostringstream
上的 I/O 操作失败时,我想短路以避免不必要地调用已经
failed
(x)stringstream
上的其余 <</>> 运算符目的。我的一些重载的
<<
/
>>
运算符会执行一些重要的格式化/解析操作,因此在失败的流对象上调用它们会浪费它们的 CPU 周期。

生成的字符串流(下面代码中的

oss
)然后被整块插入到
std::ofstream
中。我使用
std::cout
代替进行演示。

我想到了两种方法。下面是一个非常小的例子。

  1. 使用 bool 运算符进行测试
#include <bitset>
#include <sstream>
#include <iostream>
#include <cstdlib>


int main()
{
    constexpr auto delimiter { ';' };

    std::ostringstream oss;

    const bool success = oss << "Hi" &&
                         oss << delimiter &&
                         oss << 8.02 &&
                         oss << delimiter &&
                         oss << false &&
                         oss << delimiter &&
                         oss << std::bitset<10> { 0b01010 } &&
                         oss << delimiter &&
                         oss << '\n';

    if ( success ) std::cout << oss.view(); // Hi;8.02;0;0000001010;
    else return EXIT_FAILURE;
}
  1. 抛出异常
int main()
{
    constexpr auto delimiter { ';' };

    std::ostringstream oss;
    oss.exceptions( std::ios::failbit | std::ios::badbit );

    try
    {
        oss << "Hi"
            << delimiter
            << 8.02
            << delimiter
            << false
            << delimiter
            << std::bitset<10> { 0b01010 }
            << delimiter
            << '\n';
    }
    catch ( const std::ios_base::failure& e )
    {
        return EXIT_FAILURE;
    }

    std::cout << oss.view(); // Hi;8.02;0;0000001010;
}

显然,编译器为这两种方法生成不同的代码(基于异常的方法的代码明显更少),尽管我不确定在上述用例中哪一种方法可以更快。

哪一个可能是更好的解决方案?我必须向流对象插入/提取大约 20 个值。如果做出正确的选择,可能会提高效率。

c++ performance error-handling c++20 stringstream
1个回答
0
投票

由于“什么更快”这个问题无法回答,我将尝试解释每种情况下发生的情况。尽管如此,我还是想强烈强调,这个答案应该被视为学术上的考虑,你不应该仅仅根据你在这里读到的内容来引入代码更改。分析您的代码,找到瓶颈,修复它们,当对可能的修复有疑问时,对它们进行基准测试(或同时实现并再次分析)。永远不要基于预感某些事情可能会很慢而进行优化。

你必须回答的问题是“我想阻止什么?”。如果您试图阻止在流处于失败状态时调用内置

operator<<
的主体,那么正如 Pete 在评论中指出的那样,这已经为您完成了。这意味着,不会尝试将数值转换为字符串表示形式,也不会将任何字符串复制到流中,也不会发生对输出或文件的任何操作。如果您对自己的类型进行重载
operator<<
,并且这些重载在将结果传递给内置运算符之前做了重要的工作,那么该工作仍然会发生)。 示例:

std::ostream& operator<<(std::ostream& os, const MyType1& t) {
    return os << t.a << ' ' << t.b << ", " << t.c; // almost nothing is done if !os
}

std::ostream& operator<<(std::ostream& os, const MyType2& t) {
    return os << t.toString(); // `toString` has to be evaluated fully before os is checked for fail
}

但是,如果您想阻止对操作数求值到

operator<<
(例如,构造临时对象),那就更成问题了。您的第一种方法确实可以防止在
std::bitset<10>
进入失败状态后构造任何类似
oss
的对象。这是以在汇编中添加多次跳转为代价的,而且跳转也不利于性能。由于新的评估规则顺序将排序添加到
operator <<
,您的第二种方法只能保证在 C++17 及更高版本中执行相同的操作。在 C++17 之前,未指定是否创建所有剩余对象。

所以,总结一下:

  1. 方法 1 确实可以防止流失败后对操作数求值,因此它确实可以防止额外的对象构造或在上面的示例中调用
    toString()
    。代价是在汇编代码中添加更多分支,这通常不是最好的性能(编译器可能会也可能不会优化以假设流不太可能失败)。
  2. 自 C++17 起,方法 2 确实会阻止在流失败后对操作数 1 求值。失败后的一个操作数仍将被评估,因为它仅在下一次读取发生在失败的流上时抛出。代价是在代码中添加异常,这通常非常昂贵。
  3. 在 C++17 之前,方法 2 不保证参数的求值。如果编译器认为它会更快,则可以选择在使用
    oss
    执行任何操作之前评估所有操作数。

为了在示例中展示它,我们假设此输出以某种方式失败:

    const bool success = oss << "Hi" &&
                         oss << delimiter && // this one failed
                         oss << std::bitset<10> { 0b01010 } &&
                         oss << delimiter &&
                         oss << std::string(13, 'n');
                         oss << '\n';

方法 1 不会创建

std::bitset
str::string
对象,方法 2 将创建
std::bitset
并在 C++17 中立即抛出(避免创建
std::string
),在 C++17 之前,方法 2 取决于编译器 - 将创建
std::bitset
std::string
可能会也可能不会。

© www.soinside.com 2019 - 2024. All rights reserved.