CppCon 2016 学习:The Exception Situation

你这段内容主要在讨论 函数接口设计时,如何表达"失败"或"不满足前提条件"的情况,特别是处理除法时除数为零的问题。以下是对关键点的理解总结:

1. 函数自然接口(Natural Interface)

函数接口定义得很简单:

cpp 复制代码
int integral_div(int num, int denom);

表示计算并返回 num / denom 的整数商(忽略余数)。

理想状态下,调用者传入两个参数,就得到结果。

2. 问题:denom == 0怎么办?

  • 除数为零是非法操作,会导致程序错误。
  • 这是一个边界或异常情况,需要处理。

3. 用前置条件表达接口约束

cpp 复制代码
// Precondition: denom != 0
int integral_div(int num, int denom);
  • 这里声明:调用时必须保证 denom 不为零。
  • 责任转移到调用者,调用者必须保证满足这个条件。
  • 如果调用者不遵守,结果未定义(可能程序崩溃)。

4. 初学者的误区:打印错误信息然后继续执行

cpp 复制代码
int integral_div(int num, int denom) {
    if (!denom)
        std::cerr << "Divide by zero" << std::endl;
    return num / denom;
}
  • 这种写法看似提示错误,但其实根本没阻止错误发生。
  • 除零后程序可能崩溃,打印没意义。
  • 这表明函数与调用者的交互不清晰。

5. 激进的处理:直接终止程序

cpp 复制代码
int integral_div(int num, int denom) {
    if (!denom) {
        std::exit(-1);  // 或 abort(), terminate() 等
    }
    return num / denom;
}
  • 这是激进的防御性编程。
  • 保证函数只在有效输入时运行,否则程序立即退出。
  • 适合一些关键系统,但通常不推荐直接杀死程序。

总结理解

  • 函数接口最好明确约束(前置条件),让调用者负责提供合法参数。
  • 函数内部不应简单"打印错误"然后继续执行,那样无效且不安全。
  • 激进措施(退出程序)虽简单粗暴,但不能解决所有场景。
  • 理想情况是用更优雅的错误处理机制(比如异常、返回错误码、或其他错误传播机制)来表达失败。
    如果你需要,我可以帮你写几个示例,展示如何更合理地表达函数的失败(比如用异常或返回 std::optional)。需要吗?

这段内容继续讲述了如何在函数接口中表达"失败"或"不满足前置条件"的情况,同时给出了多种不同的设计方案及其利弊。下面是逐条理解总结:

1. 使用 assert 断言

cpp 复制代码
int integral_div(int num, int denom) {
    assert(denom && "divide by zero");
    return num / denom;
}
  • 断言检查 denom != 0,不满足时程序终止。
  • 这是激进的"防御式"编程,保证只有在合法输入下才执行。
  • 缺点:assert 在 Release 模式通常会被编译器去掉,不会检查。
  • 需要开发阶段彻底测试,保证调用前就不会传入非法值。

2. 客户端代码示例

cpp 复制代码
int main() {
    int n, d;
    if (cin >> n >> d)
        cout << integral_div(n,d) << endl;
}
  • 如果输入为 0,会因为断言失败而程序终止。
  • 这种设计保持了函数的自然接口(两个参数,返回结果),调用简单。

3. 改变函数签名,添加"成功标志"参数

cpp 复制代码
int integral_div(int num, int denom, bool &ok) {
    if (!denom) {
        ok = false;
        return -1; // 随意返回一个值
    }
    ok = true;
    return num / denom;
}
  • 通过引用参数 ok 通知调用者是否成功。
  • 缺点:增加了函数参数,调用变得复杂。
  • 需要调用者检查 ok,否则可能忽略错误。

4. 客户端代码示例

cpp 复制代码
int main() {
    int n, d;
    if (cin >> n >> d) {
        bool ok = true;
        int res = integral_div(n,d,ok);
        if (ok)
            cout << res << endl;
    }
}
  • 代码更复杂,调用者必须管理额外的成功标志。
  • 容易忘记检查 ok,导致潜在错误。

5. 改变函数签名,返回成功码,输出结果通过引用参数

cpp 复制代码
bool integral_div(int num, int denom, int &res) {
    if (!denom)
        return false;
    res = num / denom;
    return true;
}
  • 这样调用时,返回值表示是否成功,结果通过引用参数返回。
  • 这种方式更符合"布尔值表示成功/失败"的习惯。
  • 但函数签名依然和自然接口不同。

6. 客户端代码示例

cpp 复制代码
int main() {
    int n, d;
    if (cin >> n >> d) {
        int res;
        if (integral_div(n,d,res))
            cout << res << endl;
    }
}
  • 调用代码更清晰,逻辑明显:先判断是否成功,再使用结果。
  • 但如果调用者忘记检查返回值,依然会有问题。
  • C++17 引入的 [[nodiscard]] 属性可以帮助避免忽略返回值。

总结理解

  • 使用断言简单明了,保持了自然接口,但断言只适合调试阶段,不能依赖于生产环境。
  • 添加成功标志参数 ,或者返回 bool 并通过引用参数输出结果,是比较常用的表达失败的方式,但会改变函数的自然接口。
  • 这类设计使得调用更安全,但调用者负担变重,代码更冗长。
  • 最理想的是函数接口简洁,且失败信息不能被调用者忽略,这正是异常机制的优势所在(本节未提及,但可以理解为后续改进方向)。

这段内容主要讲的是如何优雅地表达函数"失败"的情况,特别是针对integral_div(num, denom)函数中,除数denom可能为0时,如何设计函数返回值,让调用者能够知道计算是否成功,而不是简单地返回一个整数或直接崩溃。

关键点总结:

  1. 改变返回类型以表达失败

    • 使用 std::pair<bool, int> (或类似结构)

      cpp 复制代码
      std::pair<bool,int> integral_div(int num, int denom) {
          if (denom == 0) 
              return {false, -1};
          return {true, num / denom};
      }

      调用时检查bool标志判断是否成功。类似Go语言的返回值模式(返回值+错误码)。

    • 使用 C++17 的结构化绑定简化调用代码

      cpp 复制代码
      if (int n, d; std::cin >> n >> d) {
          if (auto [ok, res] = integral_div(n, d); ok) {
              std::cout << res << std::endl;
          }
      }
  2. 使用 std::optional<int> 来表示可能失败的结果

    • 返回optional<int>,若除数为0返回空的optional,表示无有效结果。

      cpp 复制代码
      std::optional<int> integral_div(int num, int denom) {
          if (denom == 0) return {};
          return num / denom;
      }
    • 调用时检查是否有值:

      cpp 复制代码
      auto res = integral_div(n, d);
      if (res) {
          std::cout << res.value() << std::endl;
      }
    • C++17结构化绑定简化调用:

      cpp 复制代码
      if (auto res = integral_div(n, d); res) {
          std::cout << res.value() << std::endl;
      }
  3. 使用类似expected<T, E>的模式(C++标准中暂时没有)

    • expected类型包含成功时的结果或错误类型

      cpp 复制代码
      class divide_by_zero{};
      expected<int, divide_by_zero> integral_div(int num, int denom) {
          if (denom == 0) return divide_by_zero{};
          return num / denom;
      }
    • 调用时像optional一样检查是否有有效结果,同时还能得到具体的错误类型。

    • 这比optional更通用,可以传递具体错误信息(比如std::error_code),而不仅仅是"有/无"。

这几种设计思想的核心目的:

  • 避免函数内部"直接打印错误"、"终止程序"等不灵活的错误处理手段,而是把错误信息通过返回值传递给调用者,让调用者决定如何处理。
  • 让接口更安全,避免非法输入导致程序崩溃。
  • 让调用者可以清晰地检测到函数是否成功执行,并安全地访问结果。

你理解的关键点:

  • 函数自然接口: 简单的int integral_div(int num, int denom),但不表达失败。
  • 表达失败的改进接口: 返回一个能表达成功/失败的类型,如pair<bool,int>optional<int>expected<int,Error>等。
  • 客户端代码通过检查返回值状态,决定下一步操作。

这部分内容继续探讨了如何优雅地表达函数执行失败的情况,但这次是用**抛异常(throw)**的方式来处理错误,比如除以零的情况。

核心内容理解:

  1. 用异常来表达错误

    cpp 复制代码
    class divide_by_zero {};
    int integral_division(int num, int denom) {
        if (!denom)
            throw divide_by_zero{};
        return num / denom;
    }
    • 如果除数为0,就抛出一个divide_by_zero类型的异常。
    • 这样函数的接口(签名)看起来完全没变,依然是int integral_division(int, int)
    • 但是调用时,如果遇到除零,程序会跳转到异常处理逻辑。
  2. 调用代码例子

    cpp 复制代码
    int main() {
        int n, d;
        if (std::cin >> n >> d)
            std::cout << integral_division(n, d) << std::endl;
    }
    • 这里调用函数看起来和原来完全一样,没有像之前那样显式处理错误的返回值。
    • 但是,程序运行时如果遇到denom == 0,会抛异常。
  3. 疑问:没有try/catch块怎么办?

    • 代码示例中没有显式的trycatch块,异常会向上传递到调用栈外层。
    • 如果最终没有捕获异常,程序会异常终止(通常会调用std::terminate)。
    • 这是这段内容隐含的思考:如果用异常机制,调用代码要么包裹try/catch捕获异常,要么允许异常继续传播。

这和之前用optionalpair的区别

  • 异常机制的优点:
    • 保持函数接口简洁,没有改变函数返回值类型。
    • 错误处理和正常逻辑分离,调用者可以选择在哪里捕获异常。
  • 缺点:
    • 需要调用者意识到可能有异常,需要写异常处理代码,否则程序会崩溃。
    • 异常机制的运行时开销较大。
    • 异常控制流程不如返回值直观,可能导致隐藏的异常路径。

总结

  • 异常是另一种表达"失败"的手段,属于"非局部错误处理"机制。
  • 它让函数接口保持原样,不必返回"可能失败"的类型。
  • 调用者需要考虑是否捕获异常。
  • 和用optionalexpected等"显式返回错误"方法相比,异常更隐式,但更灵活。

这部分内容讲了异常的"捕获(catch)"原则和设计示例(一个简单的动态数组类 Array),主要有两个核心点:

1. 异常处理的哲学:何时捕获异常?

  • "大多数情况下,没有必要捕获异常。"
    只有当你能够"做些什么"时,才去捕获异常,否则直接让它继续传播。
  • 捕获了异常不一定马上处理它 ,如果当前层不知道怎么处理,可以重新抛出异常(rethrow),让更上层的代码去处理。
  • 这样做的好处是异常处理代码更聚焦,错误不会被"无意义的捕获"而吞掉。

2. 示例:简单的动态数组 Array<T>

  • 基本结构
    • elems:指向存储元素的动态数组
    • nelems:当前元素数量
    • cap:数组容量
  • 常用方法
    • size(), capacity() 返回大小和容量
    • begin(), end() 返回迭代器
    • full() 判断是否已满
    • push_back(const T&) 插入元素
    • grow() 扩容方法(当满了时调用)
  • grow() 的实现思路
    • 新容量 = 当前容量的两倍(或者初始64)
    • 新开辟一个数组 p
    • 复制旧元素到新数组
    • 释放旧数组内存
    • 指针和容量更新到新数组和新容量

结合异常处理:

  • 这段代码很"天真",没有专门的异常安全设计,比如:
    • 如果new T[new_cap]失败会抛异常(std::bad_alloc
    • std::copy中元素复制可能抛异常
    • 如果异常发生在中间,可能导致内存泄漏或状态不一致
  • 理想情况下,grow()要做到异常安全 (比如使用std::vector那样的策略,或者先分配新内存,再复制,成功后才切换指针)。
  • 异常处理设计原则:
    你不必在所有地方捕获异常,除非你能恢复或做补救。否则让异常往上传递,由更高层统一处理或终止程序。

总结

  • 异常不是用来随便捕获的,只捕获能做实事的。
  • 示例的动态数组展示了典型的"可能抛异常的操作",提醒我们设计时要考虑异常安全。
  • 这帮助理解如何设计既方便又安全的接口与异常策略。

这段内容讲了用RAII(资源获取即初始化)来优雅地管理资源和异常安全,并介绍了异常处理机制中的特殊情况。

1. 传统的 grow() 实现(显式异常处理)

cpp 复制代码
void grow() {
    std::size_t new_cap = capacity() ? capacity() * 2 : 64;
    auto p = new T[new_cap];
    try {
        std::copy(begin(), end(), p);
    } catch (...) {
        delete[] p;
        throw;
    }
    delete[] elems;
    elems = p;
    cap = new_cap;
}
  • 这里先用 new 分配新内存,拷贝旧元素。
  • 拷贝过程中可能抛异常,所以用 try...catch 捕获,异常时释放刚分配的内存,避免泄漏,再重新抛出异常。
  • 代码比较冗长,异常处理显得繁琐。

2. 用 RAII 简化资源管理

cpp 复制代码
void grow() {
    std::size_t new_cap = capacity() ? capacity() * 2 : 64;
    std::unique_ptr<T[]> p { new T[new_cap] }; // RAII 自动管理内存
    std::copy(begin(), end(), &p[0]);
    delete[] elems;
    elems = p.release(); // 释放智能指针控制权,内存交给 elems 管理
    cap = new_cap;
}
  • 使用 std::unique_ptr<T[]> 自动管理新分配的内存。
  • 如果 std::copy 抛异常,p 会自动析构,释放内存,不会泄漏。
  • 不用手动写异常处理代码,更简洁更安全。
  • 体现了RAII的强大------资源和异常安全自动管理。

3. 关于异常处理的特殊情况(std::terminate

标准说明中提到,当异常处理机制遇到以下情况时,必须放弃异常处理,调用 std::terminate()

  • (1.1) 异常机制在构造异常对象完成后、异常被捕获处理前,调用了又抛异常的函数(异常传播链中再次抛异常)。
  • (1.2) 找不到合适的异常处理器(catch块)处理抛出的异常。
    在这两种情况下,程序会调用 std::terminate(),通常意味着程序非正常终止。
  • 这提醒我们,异常处理并不是万能的,某些情况下异常机制会停止运行,程序崩溃。
  • 设计异常时,要注意避免"异常中再抛异常"的情形。

总结

  • RAII 是C++中实现异常安全和资源管理的利器,减少手动异常处理代码。
  • std::unique_ptr 管理内存,拷贝失败时自动释放,防止内存泄漏。
  • 异常处理机制有边界和限制,异常处理失败时调用 std::terminate()
  • 编写异常安全代码时,要考虑这些特殊情况。

这段内容主要讲在C++异常处理及错误表达中不同方案的设计权衡和效率问题,我帮你总结理解:

1. 关于异常处理和 noexceptstd::terminate

  • 如果异常传播进入了一个 noexcept 限定的函数(该函数声明不允许抛异常),且该异常未被捕获处理,标准规定:
    • 编译器是否会展开(unwind)调用栈,以及展开程度,是实现定义的。
    • 但在其他情况下(没有 noexcept 限定),异常传播失败时必须调用 std::terminate(),且不能提前停止展开调用栈。
  • 意味着对带有 noexcept 的函数,异常处理行为可能因编译器实现不同而不同。

2. 错误处理的总结:

  • 仅打印错误信息通常不够好,程序会"悬着",用户和开发者都不知道发生了什么。
  • **终止程序或断言(assert)**是一种合理方案,适合一些快速失败、重启的场景,也便于开发时捕捉错误。
  • 修改函数签名,让返回值携带状态(如C风格的返回码)是经典做法,需要调用者主动检查,且要保证调用者有良好习惯("纪律")。
  • 丰富返回类型 ,用 pair, optional, expected 等封装结果和状态,要求调用者显式检查错误,但接口依旧简洁。
  • 抛异常 ,不改接口,错误直接通过异常机制传播,调用者用 try/catch 处理。

3. 效率(constexpr)考量:

  • 打印和程序终止(terminate, exit, abort)无法作为 constexpr 函数使用,因为这些操作不是编译时可执行的。

  • 使用 assert 可以写成 constexpr,因为它在编译时有条件能触发断言:

    cpp 复制代码
    constexpr int integral_div(int num, int denom) {
        return assert(denom != 0), num / denom;
    }
  • 修改函数签名,比如多返回一个 bool &ok 状态参数,可以写成 constexpr

    cpp 复制代码
    constexpr int integral_div(int num, int denom, bool &ok) {
        if (!denom) {
            ok = false;
            return -1;  // arbitrary
        }
        ok = true;
        return num / denom;
    }
  • 返回类型是字面类型(如 pair<int,bool>)的版本也能写成 constexpr

    cpp 复制代码
    constexpr std::pair<int,bool> integral_div(int num, int denom) {
        return !denom ? std::make_pair(-1, false) : std::make_pair(num/denom, true);
    }
  • optionalexpected 通常因为有非平凡析构函数,不适合做 constexpr

总结:

  • 错误处理方式设计有多种,折中于易用性、语义表达、异常安全和效率。
  • constexpr 在错误处理方案中约束了选择,简单的返回码或字面类型封装能支持,复杂的错误封装类型则不行。
  • noexcept 的异常传播和 std::terminate() 行为是实现依赖,需注意。

"非平凡析构函数"(non-trivial destructor)是C++术语,简单说就是编译器不能自动生成的简单析构函数,而是用户自定义的或编译器生成但比较复杂的析构函数。它有以下特点和影响:

1. 平凡(trivial)析构函数 vs 非平凡析构函数

  • 平凡析构函数 (trivial destructor):
    • 编译器自动生成,没有任何用户自定义代码。
    • 不做任何操作,直接释放对象内存即可。
    • 通常用于简单的、只含基本类型成员或不需要释放资源的类型。
    • 编译器可以进行一些优化,比如允许对象放在只读内存区,允许 constexpr 构造和析构。
  • 非平凡析构函数 (non-trivial destructor):
    • 用户显式定义了析构函数,或类含有成员变量自身的析构函数非平凡。
    • 需要执行用户代码,比如释放动态资源(内存、文件句柄等)。
    • 编译器必须调用析构函数代码,不能简单地忽略。
    • 这种析构函数使得类对象的生命周期管理更复杂。

2. 对 constexpr 的影响

  • C++ 标准要求 constexpr 析构函数必须是平凡的 析构函数(trivial destructor)或者符合某些条件的 constexpr 析构函数。
  • 大多数标准库容器或包装类型(如 std::optionalstd::expected)因为需要管理资源和复杂状态,它们的析构函数是非平凡的,因此不支持在 constexpr 函数中使用(特别是在C++14之前)。
  • 反过来,如果一个类型的析构函数是平凡的,且满足其他 constexpr 约束,则可以用于 constexpr 函数和常量表达式。

3. 如何判断?

一个类的析构函数是平凡的,通常满足:

  • 没有自定义析构函数;
  • 所有非静态数据成员的析构函数也是平凡的;
  • 没有虚析构函数。
    例如:
cpp 复制代码
struct A {
  int x;
  // 平凡析构函数
};
struct B {
  ~B() {}  // 用户自定义析构函数,非平凡析构函数
};
struct C {
  std::string s;  // std::string 的析构函数非平凡
  // 因此C的析构函数也是非平凡
};

总结:

  • 非平凡析构函数意味着析构时需要执行额外代码,无法被编译器简单忽略。
  • 这会影响类型能否用于 constexpr 函数中,因为 constexpr 要求对象生命周期简单明确。
  • 这也是为什么 optionalexpected 这种封装复杂状态的类型不能轻易用作 constexpr 的原因。

这部分内容重点讲了异常的设计目的、对代码路径的影响,以及错误处理效率和代码清晰度的权衡,我帮你总结理解:

1. constexpr 与抛异常

  • 带抛异常的 constexpr 函数是允许的 (比如C++11起就支持),只不过:

    • 在没有异常时,函数可在编译期计算(即常量表达式)。
    • 一旦触发异常,计算就转为运行时处理(throw)。
  • 例如:

    cpp 复制代码
    constexpr int integral_div(int num, int denom) {
        return !denom ? throw divide_by_zero{} : num / denom;
    }
  • 这种写法兼顾了编译期常量求值与运行时异常处理。

2. 异常设计目的:错误检测与错误处理分离

  • 检测错误的地方和处理错误的地方不一定是同一个。
  • 例如 integral_div 能检测除零,但不负责决定"除零错误怎么办"。
  • 错误处理可能是:
    • 打印信息(比如日志)
    • 弹窗警告用户
    • 触发紧急停止(核反应堆停止等)
  • 异常机制让错误处理从正常代码路径中剥离出来,不用到处插入错误检查,减少代码污染。

3. 错误处理如何影响代码路径的"干净度"

  • 使用错误码(HRESULT等)和显式检查,会导致"错误处理代码"穿插在正常流程中,看起来很杂乱,且容易遗漏处理。
  • 代码示例中演示了COM接口常用的HRESULT错误检查写法,显得冗长且容易错。
  • 这是一种经典问题:错误处理代码污染正常业务代码,使代码难读难维护
  • Knuth建议用goto跳转简化错误处理,Armstrong(Erlang作者)认为错误应并行处理,不应污染主流程。

4. 错误处理永远不会消失

  • 不论用异常、错误码还是其他机制,错误都可能发生。
  • 不能假设"代码永远不会错",错误处理应设计得易于维护和阅读。
  • 异常就是为了解决这一痛点,通过机制把错误处理从主流程里分离出来。

总结

  • constexpr 可以用异常,异常路径在运行时执行,非异常路径可编译时计算。
  • 异常的本质是将错误检测与错误处理分离,减少正常代码的污染。
  • 传统错误码机制导致代码里充满检查和分支,影响可读性和维护性。
  • 理想状态是错误处理"异步"于主流程,主流程保持简洁。
  • 但是在现实中,错误处理难以完全隐藏,设计要兼顾效率、可维护性和正确性。

异常(Exceptions)的优缺点以及性能成本,帮你总结和理解如下:

Exceptions --- 优点

  • 不改变函数的自然接口
    异常不会通过改变函数签名来处理错误(除非加上 noexcept),调用接口更干净。
  • 为异常情况提供独立代码路径
    异常代码路径和正常代码路径分开,不会污染主业务逻辑。
  • 将错误检测与错误处理分离
    抛出异常只是通知错误发生,具体怎么处理需要上下文环境决定。
  • 构造函数可以用异常传递错误
    构造函数没有返回值,用异常是传递错误的合理方式。

Exceptions --- 缺点

  • 并非人人喜欢异常机制
    有人觉得异常机制复杂且难以管理。
  • 异常创建了另一条代码路径
    这是优点也是缺点,代码逻辑更复杂。
  • 异常有非零开销
    主要是发生异常(throw、catch)时的成本,不是正常执行路径的成本。
  • 异常可能被滥用
    像语言中的其他特性一样,使用不当会导致问题。
  • 异常和非异常安全代码的边界问题
    例如与C语言、其他语言的库或工具交互时,异常处理比较困难。
  • Lippincott函数 是应对这种边界问题的一个工具。

Exceptions --- 性能成本的实测与分析

  • 多个测试案例比较了异常和无异常代码在不同情况下的性能:
    • 生成大量数据,偶尔出错时抛异常与返回错误码的开销比较。
    • 多层递归函数调用时异常的堆栈展开成本。
    • 复杂数据结构(vector, vector<vector>)下异常处理成本。
    • 错误频率不同(频繁出现、偶尔出现、从不出现)的性能对比。
  • 结论是:
    异常的主要开销在于发生异常时的堆栈展开和异常处理代码执行
    在正常执行路径(无异常时)性能影响较小。

理解总结

  1. 异常机制设计为正常路径开销小,但错误路径开销可能大。
  2. 错误不频繁时,异常机制整体效率较高。
  3. 异常代码路径增加代码复杂度,但能让正常业务逻辑更清晰。
  4. 异常和错误码各有利弊,选择需权衡易用性、性能和代码可维护性。

这段内容主要介绍了标准库中异常的使用情况,以及一个非常有趣的技巧------用异常机制做类型擦除和错误处理。

1. 标准库中的异常使用

  • 标准库通常不主动抛异常,主要是因为要保持效率和简洁。
  • 例外情况是 vector.at(),它会在越界时抛 std::out_of_range
  • 还有少数情况,主要是内存分配失败时抛 std::bad_alloc

2. 异常 = 错误?

  • 在 Boost.Graph 里,有一个有趣的技巧来自 Caso Neri。
  • 这个技巧利用异常机制来实现类型擦除(type erasure)和错误管理,解决了传统方法难以实现的"安全地从基类或子类转换"的问题。

3. "any_ptr" 类的实现和工作原理(异常用作类型转换)

  • any_ptr 类通过存储一个 void* 指针和两个函数指针(用于抛出异常和销毁对象),实现了类型擦除。
  • 关键函数是 thrower,它会抛出存储的指针,借助 C++ 的异常捕获机制,捕捉特定类型的指针。
  • cast_to<T>() 函数尝试通过抛出异常并捕获来"转换"指针类型,返回正确类型的指针,或者失败时返回 nullptr
  • 析构时通过 destroyer 函数指针来释放资源。
    这种方法看起来奇特,但依赖异常机制来完成类型安全的转换,是一种聪明的设计。

4. "自诊断异常"示例

  • 代码中展示了一个有趣的用法:
    抛出一个函数指针(lambda),用于异常处理时执行特定的诊断操作。
  • 这种用法很少见,也不推荐在生产代码中使用,但很有创意。
cpp 复制代码
using diag_t = void(*)(ostream&);
int f(int n) {
  if (n < 0)
    throw static_cast<diag_t>([]{ cout << "Ouch!" << endl; });
  return n;
}
int main() {
  try {
    cout << f(-3) << endl;
  } catch(diag_t diagnosis) {
    diagnosis();
  }
}

总结

  • 标准库中的异常用得很少,主要是边界检查和内存分配失败。
  • 异常可以被巧妙地用来实现类型擦除和安全转换,尽管不常见。
  • 异常本身也能承载"行为",比如抛出一个函数指针来执行特定操作。
相关推荐
lazy H39 分钟前
从入门到日常开发,一篇文章掌握 Git 核心操作
git·后端·学习·github
Scott9999HH8 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495649 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr9 小时前
2607C++,soui与安卓
c++·soui
码智社9 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海9 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a11177612 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao12 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest