> For the complete documentation index, see [llms.txt](https://yang-fanspersonal-organization.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yang-fanspersonal-organization.gitbook.io/blog/ji-shu/qian-xi-cpp-cuo-wu-chu-li.md).

# 浅析 Cpp 错误处理

### 背景

* C++23 提案中, 通过了 `std::expected<T, E>` 来帮助处理错误，expected 的灵感主要来自于 `Rust` 中的 `std::result::Result<T, E>`, 总而言之，其基本原理为:

  ```cpp
  template <typename T, typename E>
  struct Expected {
      enum Type {
          Ok,
          Err,
      };

      Type type;
      union {
          T value;
          E error;
      };
  };
  ```

  即将返回值与错误信息放在一个union中, 要求开发者手动判断并处理错误。
* 我们知道在 C++ 中，有诸多错误处理方式, 有 `异常`, `错误码`, 选择合适的错误处理方式对于定位问题有一定的帮助。
* 在本文中，笔者通过重写一份算法来对比这三种的性能差异。

机器配置:

```
Cpu:    7950x3D (16Core32Thread) @5.8Ghz
Memory: 32GB DDR5
Os:     ArchLinux x86_64 kernel-linux-6.9.7
g++ version: 14.1.1
google_benchmark_version: 1.8.4-1
```

默认使用 g++ 编译，开启 O2 优化。

### 算法

* IP => uint32 这个是一个很常见的需求，通过压缩 ip 即可通过 4 bytes 来存储一个 ip 地址。
* 使用异常来处理:

```cpp
class ParseException : public std::exception {
   public:
    explicit ParseException(std::string&& msg) noexcept : error_msg_(std::move(msg)) {}
    explicit ParseException(const std::string& msg) noexcept
        : error_msg_(msg) {}
    explicit ParseException(const char *msg) noexcept : error_msg_(msg) {}
    [[nodiscard]] const char *what() const noexcept override { return error_msg_.c_str(); }

    std::string error_msg_;
};

uint32_t decode_ip_exception(const std::string &ip) {
    // check ip size 0.0.0.0 ~ 111.111.111.111
    if (ip.size() < 7 || ip.size() > 15) {
        throw ParseException(
            std::format("ip size {} is valid, should in (7, 15)", ip.size()));
    }
    size_t begin_idx = 0;
    std::vector<std::string> split_str;
    // split the ip
    while (true) {
        auto find_idx = ip.find('.', begin_idx);
        if (find_idx == std::string::npos) {
            auto value =
                std::string(ip.c_str() + begin_idx, ip.size() - begin_idx);
            if (value.empty()) {
                throw ParseException(std::move(
                    std::format("split number {} can't empty", value)));
            }

            split_str.emplace_back(value);

            break;
        }

        std::string value(ip.c_str() + begin_idx, find_idx - begin_idx);
        if (value.empty()) {
            throw ParseException(std::move(std::format("empty parse value is invalid")));
        }

        try {
            split_str.emplace_back(value);
        } catch (const std::exception &e) {
            throw ParseException(e.what());
        }
        // update find index
        begin_idx = find_idx + 1;
    }

    if (split_str.size() != 4) {
        throw ParseException(std::move(
            std::format("split string size {} is invalid", split_str.size())));
    }

    uint32_t result;
    for (int i = 0; i < 4; i++) {
        int v;
        try {
            v = std::stoi(split_str[i]);
        } catch (const std::exception &e) {
            throw ParseException(e.what());
        }

        if (v < 0 || v > 255) {
            throw ParseException(std::move(
                std::format("parse number: {} should in (0, 255)", v)));
        }

        (reinterpret_cast<uint8_t *>((&result)))[i] = v;
    }

    return result;
}

```

* 使用expected处理

```cpp
std::expected<uint32_t, std::string> decode_ip_expected(
    const std::string& ip) noexcept {
    if (ip.size() < 7 || ip.size() > 16) {
        return std::unexpected(
            std::format("ip size {} is valid, should in (7, 15)", ip.size()));
    }

    int begin_idx = 0;
    std::vector<std::string> split_str;
    while (true) {
        auto find_idx = ip.find('.', begin_idx);
        if (find_idx == std::string::npos) {
            auto value =
                std::string(ip.c_str() + begin_idx, ip.size() - begin_idx);
            if (value.empty()) {
                return std::unexpected(
                    std::format("split number {} can't be empty", value));
            }

            try {
                split_str.emplace_back(value);
            } catch (const std::exception& e) {
                return std::unexpected(e.what());
            }

            break;
        }
        std::string value(ip.c_str() + begin_idx, find_idx - begin_idx);
        if (value.empty()) {
            return std::unexpected(
                std::format("parse value is empty is invalid"));
        }

        try {
            split_str.emplace_back(value);
        } catch (const std::exception& e) {
            return std::unexpected(e.what());
        }

        begin_idx = find_idx + 1;
    }

    if (split_str.size() != 4) {
        return std::unexpected(
            std::format("split string size {} is invalid", split_str.size()));
    }

    uint32_t result;
    for (int i = 0; i < 4; i++) {
        int v;
        try {
            v = std::stoi(split_str[i]);
        } catch (const std::exception& e) {
            return std::unexpected(e.what());
        }

        if (v < 0 || v > 255) {
            return std::unexpected(
                std::format("parse number: {} should in (0, 255)", v));
        }

        (reinterpret_cast<uint8_t*>((&result)))[i] = v;
    }

    return result;
}
```

* 使用类似 golang 方式处理，同时返回结果和错误，通过判断错误是否为空来做处理。

```cpp
std::pair<uint32_t, std::string> decode_ip_error(
    const std::string& ip) noexcept {
    static constexpr auto ErrParseNumber = -1;

    if (ip.size() < 7 || ip.size() > 15) {
        return std::make_pair(
            ErrParseNumber,
            std::format("ip size {} is valid, should in (7, 15)", ip.size()));
    }

    size_t begin_idx = 0;
    std::vector<std::string> split_str;
    while (true) {
        auto find_idx = ip.find('.', begin_idx);
        if (find_idx == std::string::npos) {
            auto value = std::string(ip.c_str() + begin_idx, ip.size() - begin_idx);

            if (value.empty()) {
                return std::make_pair(ErrParseNumber, std::format("split number {} can't empty", value));
            }

            try {
                split_str.emplace_back(value);
            } catch (const std::exception& e) {
                return std::make_pair(ErrParseNumber, e.what());
            }

            break;
        }

        std::string value(ip.c_str() + begin_idx, find_idx - begin_idx);
        if (value.empty()) {
            return std::make_pair(ErrParseNumber, std::format("empty parse value is invalid"));
        }

        try {
            split_str.emplace_back(value);
        } catch (const std::exception & e) {
            return std::make_pair(ErrParseNumber, e.what());
        }

        begin_idx = find_idx + 1;
    }

    if (split_str.size() != 4) {
        return std::make_pair(ErrParseNumber, std::format("split string size {} is invalid", split_str.size()));
    }

    uint32_t result;
    for (int i = 0; i < 4; i++) {
        int v;
        try {
            v = std::stoi(split_str[i]);
        } catch (const std::exception& e) {
            return std::make_pair(ErrParseNumber, e.what());
        }

        if (v < 0 || v > 255) {
            return std::make_pair(ErrParseNumber, std::format("parse number: {} should in (0, 255)", v));
        }

        (reinterpret_cast<uint8_t *>((&result)))[i] = v;
    }

    return std::make_pair(result, std::string());
}
```

### benchmark

```cpp
std::vector<std::string> ip_strs;

const static int len = 10000;
void bench_expected(benchmark::State& state) {
    std::vector<uint32_t> vec_res;
    std::vector<uint32_t> vec_err;
    int idx = 0;
    for ([[maybe_unused]] auto _ : state) {
        auto res = decode_ip_expected(ip_strs[(idx++) % len]);
        if (res.has_value()) {
            vec_res.push_back(res.value());
        } else {
            vec_err.push_back({});
        }
    }
}

void bench_exception(benchmark::State& state) {
    std::vector<uint32_t> vec_res;
    std::vector<uint32_t> vec_err;
    int idx = 0;
    for ([[maybe_unused]] auto _ : state) {
        try {
            auto res = decode_ip_exception(ip_strs[(idx++) % len]);
            vec_res.push_back(res);
        }
        catch (const ParseException& e) {
            vec_err.push_back({});
        } catch (const std::exception& e) {
            vec_err.push_back({});
        }
    }
}

void bench_error(benchmark::State& state) {
    std::vector<uint32_t> vec_res;
    std::vector<uint32_t> vec_err;
    int idx = 0;
    for ([[maybe_unused]] auto _ : state) {
        auto [res, err] = decode_ip_error(ip_strs[(idx++) % len]);
        if (err.empty()) {
            vec_res.push_back(res);
        } else {
            vec_err.push_back({});
        }
    }
}

void initialize() {
    ip_strs.reserve(len);
    int i = 0;
    for (; i < (len / 4); i++) {
        ip_strs.push_back(std::format("192.168.110.{}", (i % 256)));
    }
    srand((unsigned long)(time(nullptr)));
    for (; i < (len / 2); i++) {
        ip_strs.push_back(std::to_string(rand()));
    }
    for (; i < len; i++) {
        ip_strs.push_back(std::format("192.168.110.{}", (i % 256)));
    }
}

BENCHMARK(bench_error);
BENCHMARK(bench_exception);
BENCHMARK(bench_expected);

int main(int argc, char** argv) {
    initialize();
    benchmark::Initialize(&argc, argv);
    benchmark::RunSpecifiedBenchmarks();
    return 0;
}
```

`ip_strs` 是全局的数据集，通过构造错误和正确的数据来模拟现实使用情况。需要说明的是，大多数情况而言，实际上用到的数据大多数时候是正确的。故笔者将同时构造全部正确数据和全错误数据以及混合数据，来对比二者的性能。

只需改几行代码:

```cpp
// 正常数据的初始化
void initialize() {
    ip_strs.reserve(len);
    int i = 0;
    for (; i < (len); i++) {
        ip_strs.push_back(std::format("192.168.110.{}", (i % 256)));
    }
}
```

测试 3 次:

```rust
bench_error           92.3 ns         92.2 ns      7550984
bench_exception       89.8 ns         89.7 ns      7787362
bench_expected        90.5 ns         90.4 ns      7696559

bench_error            101 ns          100 ns      6783972
bench_exception       97.6 ns         97.4 ns      7106187
bench_expected        98.2 ns         98.0 ns      7020005

bench_error            100 ns          100 ns      6920981
bench_exception       92.7 ns         92.6 ns      7151969
bench_expected        97.8 ns         97.7 ns      7051804
```

通过笔者多次测试，发现这三种算法，在数据集不发生错误时，均表现正常。但是使用异常的方式略微胜出。笔者在此猜测，由于另外两种方式对结果都需要进行判断，但是只有`exception`的方式，直接使用结果，只有当错误抛出时，才处理。这就导致了他有微弱的优势。

#### 数据 1/2 半错误测试:

```cpp
void initialize() {
    ip_strs.reserve(len);
    int i = 0;
    for (; i < (len / 4); i++) {
        ip_strs.push_back(std::format("192.168.110.{}", (i % 256)));
    }
    srand((unsigned long)(time(nullptr)));
    for (; i < (len / 2); i++) {
        ip_strs.push_back(std::format("{}.{}.{}.{}", rand(), rand(), rand(), rand()));
    }
    for (; i < len; i++) {
        ip_strs.push_back(std::format("192.168.110.{}", (i % 256)));
    }
}
```

```rust
bench_error           88.4 ns         88.3 ns      7912950
bench_exception        267 ns          267 ns      2472229
bench_expected        80.1 ns         80.0 ns      8558828

bench_error           89.1 ns         88.9 ns      7848088
bench_exception        265 ns          265 ns      2527210
bench_expected        82.4 ns         82.3 ns      8573386

bench_error           93.0 ns         92.9 ns      7517722
bench_exception        274 ns          273 ns      2533749
bench_expected        88.2 ns         88.1 ns      7982644

```

可以发现的是，异常在这里表现不是很好，他的开销大约是 error 和 expected 的 3 倍左右。

### 分析

遇事不决，flamegraph 先行。我们通过修改代码，只对 bench\_exception 进行采样。

perf 采样命令:

```bash
 perf record --freq=997 --call-graph dwarf -q -o ./xxx.data /home/sehep/github/error_bench/build/error
```

<figure><img src="/files/wQTWaij6wS9YPgZo7xt5" alt=""><figcaption><p>decode_exception 火焰图</p></figcaption></figure>

和预想的一样, decode\_exception 的 `54.53%` 的时间开销花在了 `__cxa_throw` 这个函数中。

借助神器 `CompilerExplorer` 分析汇编: [link](https://godbolt.org/z/n3v37rj1E)

<figure><img src="/files/0hpOFGtHnFMrHqobwX43" alt=""><figcaption><p>函数汇编对应</p></figcaption></figure>

可以看到 throw 的时候，做了如下几件事情:

1. 构造`ParseException（包括构造自己和基类std::exception, OFFSET FLAT:vtable这里设置了虚函数` \`what\` 的虚指针。
2. `mov edx, OFFSET FLAT:ParseException::~ParseException()`: 将 `ParseException` 的析构函数地址移动到 ``edx。这里将析构函数地址保存下来，再向外传递的时候，只有向上回溯到 `catch` 关键字才会触发析构。``
3. `mov esi, OFFSET FLAT:typeinfo for ParseException`: 将 `ParseException`类型信息的地址移动到 ``esi`。这里应该是标准的实现，为了在运行时保存类型信息。``

``当然，这里都不是重点，来看看最重要的 `__cxa_throw` 函数做了什么。``

g++ 默认使用`libstdc++`, 通过在最新的 gcc 仓库中搜索:

```c
// libstdc++-v3/libsupc++/eh_throw.cc:77-99
/**
 * @param: obj: 异常对象指针
 * @param: tinfo: 异常类型信息
 * @param: dest: 异常对象析构函数指针
 */
extern "C" void
__cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo,
			 void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
{
  PROBE2 (throw, obj, tinfo);
  // 看上去了拿到了全局管理异常的一个class
  __cxa_eh_globals *globals = __cxa_get_globals ();
  // 引用计数 +1
  globals->uncaughtExceptions += 1;
  // Definitely a primary.
  // 构造最先开始需要处理的异常
  __cxa_refcounted_exception *header =
    __cxa_init_primary_exception(obj, tinfo, dest);
  // 
  header->referenceCount = 1;

#ifdef __USING_SJLJ_EXCEPTIONS__
  _Unwind_SjLj_RaiseException (&header->exc.unwindHeader);
#else
  _Unwind_RaiseException (&header->exc.unwindHeader);
#endif

  // Some sort of unwinding error.  Note that terminate is a handler.
  __cxa_begin_catch (&header->exc.unwindHeader);
  // 如果他没有被 catch 住，
  std::terminate ();
}
```

火焰图中 `_Unwind_RaiseException` 这个函数占了 `_cxa_throw` 的 95% 时间开销。该函数处在 `libunwind` 之中。

<pre class="language-cpp"><code class="lang-cpp"><strong>// src/unwind/RaiseException.c
</strong><strong>_Unwind_Reason_Code
</strong>_Unwind_RaiseException (struct _Unwind_Exception *exception_object)
{
  uint64_t exception_class = exception_object->exception_class;
  _Unwind_Personality_Fn personality;
  struct _Unwind_Context context;
  _Unwind_Reason_Code reason;
  unw_proc_info_t pi;
  unw_context_t uc;
  unw_word_t ip;
  int ret;

  Debug (1, "(exception_object=%p)\n", exception_object);

  if (_Unwind_InitContext (&#x26;context, &#x26;uc) &#x3C; 0)
    return _URC_FATAL_PHASE1_ERROR;

  /* Phase 1 (search phase) */

  while (1)
    {
      if ((ret = unw_step (&#x26;context.cursor)) &#x3C;= 0)
        {
          if (ret == 0)
            {
              Debug (1, "no handler found\n");
              return _URC_END_OF_STACK;
            }
          else
            return _URC_FATAL_PHASE1_ERROR;
        }

      if (unw_get_proc_info (&#x26;context.cursor, &#x26;pi) &#x3C; 0)
        return _URC_FATAL_PHASE1_ERROR;

      personality = (_Unwind_Personality_Fn) (uintptr_t) pi.handler;
      if (personality)
        {
          reason = (*personality) (_U_VERSION, _UA_SEARCH_PHASE,
                                   exception_class, exception_object,
                                   &#x26;context);
          if (reason != _URC_CONTINUE_UNWIND)
            {
              if (reason == _URC_HANDLER_FOUND)
                break;
              else
                {
                  Debug (1, "personality returned %d\n", reason);
                  return _URC_FATAL_PHASE1_ERROR;
                }
            }
        }
    }

  /* Exceptions are associated with IP-ranges.  If a given exception
     is handled at a particular IP, it will _always_ be handled at
     that IP.  If this weren't true, we'd have to track the tuple
     (IP,SP,BSP) to uniquely identify the stack frame that's handling
     the exception.  */
  if (unw_get_reg (&#x26;context.cursor, UNW_REG_IP, &#x26;ip) &#x3C; 0)
    return _URC_FATAL_PHASE1_ERROR;
  exception_object->private_1 = 0;      /* clear "stop" pointer */
  exception_object->private_2 = ip;     /* save frame marker */

  Debug (1, "found handler for IP=%lx; entering cleanup phase\n", (long) ip);

  /* Reset the cursor to the first frame: */
  if (unw_init_local (&#x26;context.cursor, &#x26;uc) &#x3C; 0)
    return _URC_FATAL_PHASE1_ERROR;

  return _Unwind_Phase2 (exception_object, &#x26;context);
}
</code></pre>

大概分析一下这个函数的流程:

1. *Unwind*\_InitContext 用来初始化上下文。
2. `unw_step`不断向上查找有handler 函数的栈帧, `unw_step` 是一个宏,  最后会调用 `UNW_PASTE(UNW_PREFIX, fn)` 这个宏，最终靠 \`UNW\_PASTE2\` 将 UNW\_PREFIX 和 fn 宏拼接在一起。
   1. UNW\_PREFIX 的定义是: `#define UNW_PREFIX UNW_PASTE(UNW_PASTE(_U, UNW_TARGET), _)` 通过不断的粘贴字符串之后可以得到: `_Ux86_64_{fn}`, 这里的 fn 应该是函数名称。这里之后拼凑出来的函数，应该是被 `unw_get_proc_info` 函数使用，该函数获取当前进程栈帧的一些信息。
   2. 之后获得当前函数的 `pi.handler` 的处理回调，如果回调可用，则调用，这里判断调用之后的 reason, `_URC_HANDLER_FOUND` 如果找到就break, 否则就返回。
   3. 调用 `_Unwind_Phase2` ，该函数占用了 `_Unwind_RaiseException` 的 40% 时间开销。

```cpp
static _Unwind_Reason_Code ALWAYS_INLINE
_Unwind_Phase2 (struct _Unwind_Exception *exception_object,
                struct _Unwind_Context *context)
{
  _Unwind_Stop_Fn stop = (_Unwind_Stop_Fn) exception_object->private_1;
  uint64_t exception_class = exception_object->exception_class;
  void *stop_parameter = (void *) exception_object->private_2;
  _Unwind_Personality_Fn personality;
  _Unwind_Reason_Code reason;
  _Unwind_Action actions;
  unw_proc_info_t pi;
  unw_word_t ip;
  int ret;

  actions = _UA_CLEANUP_PHASE;
  if (stop)
    actions |= _UA_FOR
```

看上去他做了这几件事情:

* 设置 action 为 `_UA_CLEANUP_PHASE` 当前处于清理状态。
* 这部分看起来就是不断的向上追溯，使用 `unw_step` 函数尝试向前执行一步栈展开。

由于笔者对 `libunwind` 并不是非常熟悉，所以只能根据仅有的注释来看。如果读者有建议or看法，欢迎联系笔者。根据这部分代码，大概流程即可梳理为，不断向上回溯，并且寻找注册好了的函数钩子，来做处理。

### 总结

三种错误方式，基本上都可以在不同的项目中见到缩影。但是 `std::expected` 还是比较少见的。从我们的 benchmark 和 分析可以得到结论：

* 当错误不发生时, 使用 `exception` 的错误处理性能是最高的。
* 当都有错误时, `exception` 的性能退化幅度较为大，`std::expected` 和 `pair<T,E>` 的性能差不多。通过对 bench\_exception 的汇编进行分析，g++ 对于exception 会生成大量汇编代码，在大量使用异常的项目中，这很有可能增大二进制文件的体积。

一味的讨论性能可能并不是本文的唯一目的，从使用易用性角度来看，笔者认为 `exception` 由于错误抛出都在代码中，可能需要查看调用函数代码才能写出正确捕获的代码。但是如果项目统一使用异常来处理错误，那么这其实也并不是一件坏事。

`std::expected<T,E>` 作为 Rust 中通用的错误处理一直被人称赞，但这其实是和其他语法 \[带有类型的枚举], \[ 模式匹配] 相对应的。在 Cpp 中 P2688 提案(<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2688r0.pdf>)  提出了在 Cpp 实现模式匹配。笔者认为 std::expected 将会是成为大一统的错误处理方式。这种强制判断结果无疑减少了工程师的心智负担。

### 引用

1. ^ C++ cppreference std::expected [link](https://en.cppreference.com/w/cpp/utility/expected)
