跳转至

嵌入式C++教程——std::expected

写一段可靠又不振聋发聩的错误处理代码,是每个 C++ 开发者的隐秘欲望。std::expected(C++23 标准中引入)像个温柔的中介:当你要么有一个值,要么有个错误,别再用 throw、别再滥用 std::optional<T>std::variant 来凑合了——它专为这类场景设计。


开篇:为什么需要 expected

把错误当成"值"的一等公民,能让代码逻辑变得线性而明确。std::optional<T> 告诉你"有或者没有值",但没有告诉你为什么没有;std::variant<T, E> 能表达"要么这个,要么那个",但读者每次看到 variant 解包时都要心里默念三遍类型顺序。expected<T, E> 则直接表达出一种约定:成功就给你 T,失败给你 E。调用端可以显式检查、链式组合、或者优雅地传播错误——可读、可组合,还不会像异常那样把控制流抛到天外去。


一个简短、可用的 C++17 实现(精简版本,侧重可读性)

下面的实现不是标准库级别的工业化产品,但它能让你在 C++17 环境里体验 expected 的语义,并在博客或教学中直接运行与演示。

#include <utility>
#include <type_traits>
#include <stdexcept>

// 用于构造错误分支的辅助类型
template <typename E>
struct unexpected {
    E value;
};

template <typename T, typename E>
class expected {
    bool has_value_;
    union {
        T val_;
        E err_;
    } storage_;

public:
    // 构造成功值
    expected(const T& v) : has_value_(true) { new(&storage_.val_) T(v); }
    expected(T&& v) : has_value_(true) { new(&storage_.val_) T(std::move(v)); }

    // 构造错误
    expected(unexpected<E> u) : has_value_(false) { new(&storage_.err_) E(std::move(u.value)); }

    // 默认构造为错误分支需要 E 有默认值——这里不提供默认构造

    // 析构
    ~expected() {
        if (has_value_) storage_.val_.~T(); else storage_.err_.~E();
    }

    bool has_value() const noexcept { return has_value_; }

    T& value() {
        if (!has_value_) throw std::runtime_error("bad expected access");
        return storage_.val_;
    }

    const E& error() const {
        if (has_value_) throw std::runtime_error("no error present");
        return storage_.err_;
    }

    // 简单的 value_or
    T value_or(T default_value) const {
        if (has_value_) return storage_.val_;
        return default_value;
    }

    // map:将成功值用函数 f 转换为另一个 expected
    template <typename F>
    auto map(F f) const -> expected<decltype(f(std::declval<T>())), E> {
        using U = decltype(f(std::declval<T>()));
        if (has_value_) return expected<U, E>(f(storage_.val_));
        return expected<U, E>(unexpected<E>{storage_.err_});
    }

    // and_then:链式调用,f 返回 expected<U, E>
    template <typename F>
    auto and_then(F f) const -> decltype(f(std::declval<T>())) {
        if (has_value_) return f(storage_.val_);
        return decltype(f(std::declval<T>()))(unexpected<E>{storage_.err_});
    }
};

这个实现省略了大量边角(拷贝/移动语义的细粒度控制、noexceptconstexpr、更友好的 unexpected API、复杂的 SFINAE),但足够清楚地表达 expected 的核心:要么有值,要么有错误


使用示例:从解析到链式调用

来看个小例子:一个解析函数返回 expected<int, std::string>,调用端可以优雅地链式处理错误。

#include <string>
#include <iostream>

expected<int, std::string> parse_int(const std::string& s) {
    try {
        size_t pos;
        int v = std::stoi(s, &pos);
        if (pos != s.size()) return unexpected<std::string>{"trailing chars"};
        return expected<int, std::string>(v);
    } catch (...) {
        return unexpected<std::string>{"not a number"};
    }
}

int main(){
    auto r = parse_int("123");
    if (r.has_value()) std::cout << "value=" << r.value() << "\n";
    else std::cerr << "error: " << r.error() << "\n";

    // 链式示例(伪代码风格)
    auto final = parse_int("42").and_then([](int x){
        return expected<double, std::string>(x / 2.0);
    }).map([](double d){ return d * 3.0; });
}

链式写法的魅力在于:每一步只关心成功分支,错误会自动穿透并最终被处理或返回给上层逻辑。

  • 错误类型 E:推荐使用小而可拷贝的类型(错误码、短字符串、结构体)。把整个 std::exception_ptr 或重对象当作错误会让 expected 变得笨重。
  • 异常 vs 返回值:expected 非常适合库边界或性能敏感场景。在你不想用异常控制流、又希望调用 site 显式处理失败的地方,它是理想选择。
  • 链式风格:and_thenmap 让你像写函数式代码那样组织逻辑。只要注意被链的函数签名匹配。
  • std::optional 的区别:optional<T> 只是"可能没有值",并不提供错误信息。expected<T, E> 则更具语义性,能承载失败原因。

把错误当成值来处理,会让你的代码少些异常的惊吓,多些明确的控制流。C++23 的 std::expected 是标准对这一路线的肯定;而在不得不用 C++17 的时代,动手实现一个小巧的 expected,既能提升代码可读性,也能为团队带来更一致的错误处理风格。

查看完整可编译示例
// expected.hpp
// 简化的 std::expected 实现(C++17)

#pragma once
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>

// 用于构造错误分支的辅助类型
template <typename E> struct unexpected {
  E value;
};

template <typename T, typename E> class expected {
  bool has_value_;
  union Storage {
    T val_;
    E err_;
    /*
    Issue: expected compiled failed #1
    See:
    https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP/issues/1
    因为 union 包含非平凡构造/析构成员时,编译器会删除其默认构造函数和析构函数。
    导致 enclosing class 在构造 storage_ 时失败。必须手动提供空构造/析构。
    */
    Storage() {}
    ~Storage() {}
  } storage_;

public:
  // 构造成功值
  expected(const T &v) : has_value_(true) { new (&storage_.val_) T(v); }
  expected(T &&v) : has_value_(true) { new (&storage_.val_) T(std::move(v)); }

  // 构造错误
  expected(unexpected<E> u) : has_value_(false) {
    new (&storage_.err_) E(std::move(u.value));
  }

  // 拷贝/移动构造(简化版)
  expected(const expected &other) : has_value_(other.has_value_) {
    if (has_value_) {
      new (&storage_.val_) T(other.storage_.val_);
    } else {
      new (&storage_.err_) E(other.storage_.err_);
    }
  }

  expected(expected &&other) noexcept : has_value_(other.has_value_) {
    if (has_value_) {
      new (&storage_.val_) T(std::move(other.storage_.val_));
    } else {
      new (&storage_.err_) E(std::move(other.storage_.err_));
    }
  }

  // 析构
  ~expected() {
    if (has_value_)
      storage_.val_.~T();
    else
      storage_.err_.~E();
  }

  // 赋值运算符
  expected &operator=(const expected &other) {
    if (this != &other) {
      if (has_value_)
        storage_.val_.~T();
      else
        storage_.err_.~E();
      has_value_ = other.has_value_;
      if (has_value_) {
        new (&storage_.val_) T(other.storage_.val_);
      } else {
        new (&storage_.err_) E(other.storage_.err_);
      }
    }
    return *this;
  }

  bool has_value() const noexcept { return has_value_; }
  explicit operator bool() const noexcept { return has_value_; }

  T &value() {
    if (!has_value_)
      throw std::runtime_error("bad expected access");
    return storage_.val_;
  }

  const T &value() const {
    if (!has_value_)
      throw std::runtime_error("bad expected access");
    return storage_.val_;
  }

  const E &error() const {
    if (has_value_)
      throw std::runtime_error("no error present");
    return storage_.err_;
  }

  // 简单的 value_or
  T value_or(T default_value) const {
    if (has_value_)
      return storage_.val_;
    return default_value;
  }

  // map:将成功值用函数 f 转换为另一个 expected
  template <typename F>
  auto map(F f) const -> expected<decltype(f(std::declval<T>())), E> {
    using U = decltype(f(std::declval<T>()));
    if (has_value_)
      return expected<U, E>(f(storage_.val_));
    return expected<U, E>(unexpected<E>{storage_.err_});
  }

  // and_then:链式调用,f 返回 expected<U, E>
  template <typename F>
  auto and_then(F f) const -> decltype(f(std::declval<T>())) {
    if (has_value_)
      return f(storage_.val_);
    return decltype(f(std::declval<T>()))(unexpected<E>{storage_.err_});
  }
};
// parse_example.cpp
// 使用 expected 进行错误处理的示例

#include "expected.hpp"
#include <string>
#include <iostream>

// 解析整数,返回 expected<int, string>
expected<int, std::string> parse_int(const std::string& s) {
    try {
        size_t pos;
        int v = std::stoi(s, &pos);
        if (pos != s.size()) return unexpected<std::string>{"trailing chars"};
        return v;
    } catch (...) {
        return unexpected<std::string>{"not a number"};
    }
}

// 验证范围,返回 expected<int, string>
expected<int, std::string> validate_range(int x) {
    if (x < 0) return unexpected<std::string>{"negative value"};
    if (x > 100) return unexpected<std::string>{"value too large"};
    return x;
}

int main() {
    std::cout << "=== std::expected (C++17) Example ===\n\n";

    // 基本用法
    auto r = parse_int("123");
    if (r.has_value()) {
        std::cout << "Parsed: " << r.value() << "\n";
    } else {
        std::cerr << "Error: " << r.error() << "\n";
    }

    // 错误情况
    auto r2 = parse_int("abc");
    if (!r2) {
        std::cerr << "\nError parsing 'abc': " << r2.error() << "\n";
    }

    auto r3 = parse_int("123abc");
    if (!r3) {
        std::cerr << "Error parsing '123abc': " << r3.error() << "\n";
    }

    // value_or
    std::cout << "\nvalue_or demo:\n";
    std::cout << "parse_int(\"42\").value_or(-1): " << parse_int("42").value_or(-1) << "\n";
    std::cout << "parse_int(\"bad\").value_or(-1): " << parse_int("bad").value_or(-1) << "\n";

    // 链式调用
    std::cout << "\n=== Chaining ===\n";
    auto result = parse_int("50")
        .and_then(validate_range)
        .map([](int x) { return x * 2; });

    if (result) {
        std::cout << "Success: " << result.value() << "\n";
    } else {
        std::cout << "Error: " << result.error() << "\n";
    }

    // 链式调用 - 失败情况
    auto result2 = parse_int("200")
        .and_then(validate_range)
        .map([](int x) { return x * 2; });

    if (!result2) {
        std::cout << "\nError in chain: " << result2.error() << "\n";
    }

    return 0;
}