NX插件开发中的C++异常安全编程:从原理到实战

1. 项目概述:为什么NX插件开发必须关注异常安全?

如果你是一名使用C++为西门子NX 12.0开发Open API插件的工程师,那么“NX捕获到标准C++异常”这条错误日志,很可能已经成为你调试过程中的“老朋友”了。这绝不仅仅是一个普通的程序崩溃提示,它背后揭示的是一个在NX二次开发领域至关重要,却又常被新手甚至部分有经验的开发者所忽视的核心议题:异常安全编程

简单来说,异常安全编程是指你的代码在面对异常(Exception)被抛出时,能够保持程序状态的一致性,不会发生资源泄漏、数据破坏等灾难性后果。在普通的桌面应用开发中,一个未捕获的异常可能导致程序崩溃,用户重启即可。但在NX这样的复杂工业软件环境中,你的插件是运行在NX主进程内部的。一个“逃逸”的异常(即未被插件内部妥善处理,一路抛到了NX内核的异常)就像一颗在精密仪器内部引爆的炸弹,轻则导致当前操作失败、NX报错退出,重则可能破坏NX的会话状态,导致未保存的数据丢失,甚至影响NX的稳定性。NX内核捕获到这个异常后,通常会强制终止插件的执行,并记录下我们开头看到的那条日志,这实际上是一种保护机制,防止有问题的插件把整个NX搞垮。

因此,掌握异常安全编程,对于NX插件开发者而言,不是一种“高级技巧”,而是一项生存技能。它直接决定了你开发的插件是稳定可靠的工业级工具,还是一个随时可能“炸掉”用户工作进时的危险品。本文将从零基础出发,拆解在NX 12.0环境下实现C++异常安全编程的关键步骤、核心思想与实战技巧,让你不仅能解决眼前的报错,更能从设计层面构建出健壮的插件代码。

2. 核心概念解析:C++异常机制与异常安全等级

在深入NX的具体实践之前,我们必须夯实理论基础,理解C++异常是如何工作的,以及什么是“异常安全”的等级划分。这是后续所有实操的基石。

2.1 C++异常处理的基本流程

C++异常处理基于try,catch,throw三个关键字。

  1. throw:当检测到错误或无法继续执行的情况时,使用throw表达式抛出一个异常对象。这个对象可以是任何类型(如int,char*),但标准库通常使用派生自std::exception的类(如std::runtime_error)。
  2. try:将可能抛出异常的代码块用try语句包围起来。
  3. catch:紧随try块之后,定义一个或多个catch块,用于捕获并处理特定类型的异常。程序会按catch块的出现顺序进行匹配。

一个典型示例:

#include <stdexcept> #include <iostream> void riskyFunction(int value) { if (value < 0) { throw std::runtime_error("输入值不能为负数"); } // ... 正常操作 } int main() { try { riskyFunction(-5); // 这里会抛出异常 } catch (const std::runtime_error& e) { std::cerr << "捕获到运行时错误: " << e.what() << std::endl; // 进行错误恢复或清理操作 } catch (...) { // 捕获所有其他类型的异常 std::cerr << "捕获到未知异常" << std::endl; } return 0; }

在NX插件开发中,你的入口函数(如ufusrufsta)以及所有回调函数,都应该被视为一个巨大的try块的外部。任何在这些函数内部抛出且未被内部catch住的异常,都会“逃逸”到NX内核。

2.2 异常安全性的四个等级

这是理解异常安全的关键。它描述了一段代码在异常发生时的行为保证,按保证强度从低到高分为:

  1. 无保证(No guarantee):最糟糕的情况。如果抛出异常,程序可能处于任何状态——资源泄漏、数据损坏、对象失效。这是我们绝对要避免的。
  2. 基本保证(Basic guarantee):如果抛出异常,程序状态保持不变。不会发生资源泄漏,所有对象仍处于有效(但不一定是可预测的)状态。这是大多数操作应该达到的最低安全标准。
  3. 强保证(Strong guarantee):如果抛出异常,程序状态完全回滚到操作调用之前的状态。就像这个操作从未发生过一样。这通常通过“拷贝-交换”(copy-and-swap)惯用法实现。
  4. 不抛异常保证(Nothrow guarantee):承诺操作绝不会抛出异常。析构函数、内存释放操作(如delete)必须提供此保证,否则资源泄漏将无法避免。

对于NX插件开发,我们的核心目标是:确保所有暴露给NX框架的接口(回调函数、入口点)至少提供基本保证,并尽可能在关键操作上追求强保证,同时确保资源管理类(如智能指针、自定义句柄)提供不抛异常保证。

注意:很多新手会误以为“我用try-catch(...)包住所有代码就安全了”。这是错误的。catch(...)只能防止异常逃逸,但它无法自动恢复程序状态。如果异常抛出时,你已经修改了一半的全局数据或NX对象,catch块内若没有正确的回滚逻辑,程序状态依然是损坏的(即“无保证”)。异常安全的核心在于代码结构资源管理策略,而非简单的捕获。

3. NX Open C++ API 与异常安全实践

NX Open C++ API 是我们在插件中与NX交互的主要方式。理解其特性对于编写异常安全代码至关重要。

3.1 NX API 的错误报告机制:异常 vs. 错误码

这是一个非常关键的点。NX Open C++ API主要使用返回整数错误码(int类型)的方式来报告错误,而非C++异常。常见的模式是,函数返回一个int值,0表示UF_NORMAL(成功),非零值表示各种错误(如UF_FAIL)。

例如,创建一个块:

#include <uf_modl.h> tag_t block_tag = NULL_TAG; int status = UF_MODL_create_block1(..., &block_tag); if (status != 0) { // 处理错误,可能是打印日志、清理资源,然后返回错误码 char err_msg[MAX_STRING_LENGTH]; UF_get_fail_message(status, err_msg); UF_UI_write_listing_window(err_msg); return status; // 将错误码返回给上层调用者 }

这意味着,在调用NX API时,我们通常不会直接捕获到来自NX的C++异常。我们看到的“NX捕获到标准C++异常”,几乎100%是由我们自己写的C++代码抛出的,然后穿过了NX API的调用层,最终被NX内核捕获。

因此,我们的异常安全策略需要分为两层:

  1. 对NX API的调用:通过检查返回的错误码进行防御性编程,确保在NX操作失败时,我们的插件状态是可控的。
  2. 对我们自己的C++业务逻辑:使用C++异常机制进行错误处理,并确保在异常发生时,已经申请的任何NX资源或系统资源都能被正确释放。

3.2 资源管理:防止泄漏的关键

资源泄漏(内存、NX对象标签、文件句柄等)是破坏异常安全性的头号杀手。在C++中,RAII(Resource Acquisition Is Initialization,资源获取即初始化)是解决这一问题的黄金准则。

RAII的核心思想:将资源(内存、句柄、锁等)的生命周期与一个对象的生命周期绑定。在构造函数中获取资源,在析构函数中释放资源。这样,只要对象本身以正确的方式离开其作用域(无论是正常结束还是因异常栈展开),析构函数都会被自动调用,资源也就被自动释放。

在NX开发中的实践

  1. 使用智能指针管理动态内存:彻底避免使用裸newdelete

    #include <memory> // 错误做法:如果processData抛出异常,pData内存泄漏 // MyData* pData = new MyData(); // processData(pData); // 可能抛异常 // delete pData; // 正确做法:使用std::unique_ptr auto pData = std::make_unique<MyData>(); processData(pData.get()); // 即使抛异常,pData析构时也会自动delete
  2. 为NX对象标签(tag_t)封装RAII类:NX中的对象(如体、面、特征)都用tag_t(本质是整数句柄)标识。我们需要手动管理其生命周期(如删除)。创建一个简单的RAII包装器:

    class NXObjectWrapper { private: tag_t m_tag; bool m_owned; // 标记是否拥有该对象的所有权(是否需要负责删除) public: // 获取一个已存在对象的标签 explicit NXObjectWrapper(tag_t tag = NULL_TAG, bool owned = false) : m_tag(tag), m_owned(owned) {} // 在创建对象时使用 NXObjectWrapper(int creation_status, tag_t* tag, bool owned = true) : m_owned(owned) { if (creation_status == 0 && tag != nullptr) { m_tag = *tag; } else { m_tag = NULL_TAG; m_owned = false; // 创建失败,不拥有任何对象 } } ~NXObjectWrapper() { if (m_owned && m_tag != NULL_TAG) { // 注意:UF_MODL_delete_feature 等删除操作理论上不应抛异常。 // 但为了绝对安全,析构函数不应抛出任何异常。 try { UF_MODL_delete_feature(&m_tag); // 示例:删除特征 } catch (...) { // 记录日志,但禁止异常逃逸出析构函数! UF_UI_write_listing_window("警告:删除NX对象时发生异常。"); } } } // 禁止拷贝,允许移动(简化示例) NXObjectWrapper(const NXObjectWrapper&) = delete; NXObjectWrapper& operator=(const NXObjectWrapper&) = delete; NXObjectWrapper(NXObjectWrapper&& other) noexcept : m_tag(other.m_tag), m_owned(other.m_owned) { other.m_tag = NULL_TAG; other.m_owned = false; } // ... 其他成员函数,如获取tag tag_t get() const { return m_tag; } bool isValid() const { return m_tag != NULL_TAG; } }; // 使用示例 void createFeatureSafely() { tag_t feat_tag = NULL_TAG; int status = UF_MODL_create_some_feature(..., &feat_tag); NXObjectWrapper feat_wrapper(status, &feat_tag, true); // RAII对象 if (!feat_wrapper.isValid()) { // 特征创建失败,feat_wrapper析构时无事可做 throw std::runtime_error("创建特征失败"); } // ... 其他可能抛异常的操作 // 函数结束时,无论是否异常,feat_wrapper析构都会尝试删除特征 }

    实操心得:为每一种需要复杂管理的NX资源(如会话、事务、选择集)编写对应的RAII类,是提升插件稳定性的最有效投资。初期看似繁琐,但能一劳永逸地避免绝大部分资源泄漏问题。

4. 关键步骤:构建异常安全的NX插件代码结构

现在,我们将上述理论应用到NX插件开发的完整流程中,形成可复用的代码结构。

4.1 入口函数的异常安全封装

NX插件的入口点(如extern “C” DllExport void ufusr(char *param, int *retCode, int paramLen))是异常防御的第一道,也是最后一道防线。我们必须确保没有任何异常能从这里逃逸。

标准做法:在入口函数内部,立即用一个try-catch(...)块包裹所有逻辑。

extern "C" DllExport void ufusr(char *param, int *retCode, int paramLen) { // 初始化返回码为失败 if (retCode) *retCode = 1; // 假设1表示失败 try { // --- 你的插件核心逻辑开始 --- // 1. 解析参数(如果需要) // 2. 调用主要的业务函数 int internalStatus = myPluginMainLogic(param, paramLen); if (internalStatus == 0) { if (retCode) *retCode = 0; // 成功 } else { // 业务逻辑返回错误,可能已经处理了错误信息 if (retCode) *retCode = internalStatus; } // --- 你的插件核心逻辑结束 --- } catch (const std::exception& e) { // 捕获所有标准异常,记录到NX信息窗口和系统日志 UF_UI_write_listing_window(e.what()); // retCode 已在开头设置为失败,此处无需再改 // 确保这里没有可能再抛异常的操作! } catch (...) { // 捕获所有其他未知异常 UF_UI_write_listing_window("发生未知类型的C++异常。"); // retCode 已在开头设置为失败 } // 函数结束,无论是否异常,栈上的RAII对象都会自动清理 }

为什么要在入口点捕获所有异常?因为NX内核期望你的入口函数正常返回。如果异常逃逸到NX内核,NX会强制终止插件,你连在列表窗口输出错误信息的机会都没有,用户只会看到那个令人困惑的“捕获到标准C++异常”日志。在入口点捕获,至少给了你一个机会向用户报告一个相对友好的错误信息,并确保程序以可控的方式退出。

4.2 业务逻辑层的异常与错误码混合处理策略

在插件内部,我们推荐一种混合策略:在模块内部使用C++异常进行错误传递,在模块边界(尤其是调用大量NX API的函数)将异常转换为错误码

// 底层工具函数,可能使用异常 namespace internal { std::vector<double> parseComplexData(const std::string& input) { if (input.empty()) { throw std::invalid_argument("输入字符串为空"); } // ... 复杂的解析逻辑,可能抛异常 return result; } } // 中层业务函数,负责调用NX API和内部函数,将异常转换为错误码 int createNXFeatureWithData(const std::string& dataStr) { try { // 1. 调用可能抛异常的解析函数 auto data = internal::parseComplexData(dataStr); // 可能抛 std::invalid_argument // 2. 调用NX API(使用错误码) tag_t featTag = NULL_TAG; int status = UF_MODL_create_some_feature(..., data.data(), ..., &featTag); if (status != 0) { // NX API调用失败,这不是C++异常,而是错误码。 // 我们可以选择记录日志,然后返回错误码。 UF_UI_write_listing_window("NX特征创建失败。"); return status; // 将NX错误码向上返回 } // 3. 使用RAII对象管理新创建的特征 NXObjectWrapper featWrapper(status, &featTag, true); // ... 其他操作 return 0; // 成功 } catch (const std::invalid_argument& e) { // 捕获来自底层逻辑的异常,转换为错误码和友好信息 UF_UI_write_listing_window(e.what()); return MY_CUSTOM_ERROR_INVALID_ARG; // 返回自定义错误码 } catch (const std::bad_alloc& e) { // 内存不足异常 UF_UI_write_listing_window("内存不足,无法处理数据。"); return MY_CUSTOM_ERROR_NO_MEMORY; } catch (const std::exception& e) { // 捕获其他所有标准异常 UF_UI_write_listing_window(std::string("内部错误: ") + e.what()); return MY_CUSTOM_ERROR_INTERNAL; } catch (...) { UF_UI_write_listing_window("发生未知内部异常。"); return MY_CUSTOM_ERROR_UNKNOWN; } // 注意:此函数本身不再向外抛异常,所有错误都通过返回码表示。 } // 顶层主逻辑函数 int myPluginMainLogic(char* param, int paramLen) { // 这里主要进行流程控制和调用中层函数 std::string data(param, paramLen); int status = createNXFeatureWithData(data); if (status != 0) { // 中层函数已经处理了错误信息,这里可以补充或直接返回 return status; } // ... 其他步骤 return 0; }

这种策略的好处是清晰的职责分离:底层纯逻辑用异常(更符合C++风格,错误处理代码分离),中层与NX交互的函数负责将异常“消化”为NX插件框架能理解的错误码,顶层负责流程。

4.3 实现“强保证”的常用技巧:拷贝与交换

对于某些关键操作,我们需要实现“强保证”(事务性)。一个经典方法是“拷贝-交换”(Copy-and-Swap)。

假设我们有一个管理内部配置的类PluginConfig,修改它的操作需要是原子性的。

class PluginConfig { private: std::vector<int> m_ids; std::string m_name; // ... 其他数据成员 public: // 修改配置的强保证函数 void updateConfig(const std::vector<int>& newIds, const std::string& newName) { // 第一步:在副本上执行所有可能失败的操作 PluginConfig temp(*this); // 拷贝构造当前对象(可能抛异常,但原对象不变) temp.m_ids = newIds; // 可能抛异常(内存分配),但temp是副本 temp.m_name = newName; // 可能抛异常,但temp是副本 // 第二步:所有可能失败的操作都成功了,现在进行不会失败的交换 swap(*this, temp); // 假设swap提供不抛异常保证 // 使用 std::swap 对于标准类型通常是不抛异常的 } void swap(PluginConfig& other) noexcept { using std::swap; swap(m_ids, other.m_ids); swap(m_name, other.m_name); } };

在NX开发中,对于复杂的模型修改操作,如果NX API支持,可以先在临时模型或副本上进行操作,所有步骤成功后再通过一个原子性的NX操作(如特定特征的编辑或替换)来更新主模型。这需要深入理解特定NX API的行为。

5. 实战:一个完整的异常安全NX命令开发示例

让我们通过一个简化但完整的例子,将上述所有原则串联起来。这个命令的功能是:用户选择一个圆柱面,插件读取其半径和高度,然后创建一个同心的新圆柱体。

5.1 头文件定义(safe_create_cylinder.h)

#pragma once #include <uf.h> #include <uf_ui.h> #include <uf_modl.h> #include <memory> #include <string> #include <vector> // RAII包装器:用于管理NX选择操作 class NXSelectionWrapper { public: NXSelectionWrapper() = default; ~NXSelectionWrapper() noexcept; // 析构函数确保恢复选择模式 // 禁用拷贝,允许移动 NXSelectionWrapper(const NXSelectionWrapper&) = delete; NXSelectionWrapper& operator=(const NXSelectionWrapper&) = delete; NXSelectionWrapper(NXSelectionWrapper&& other) noexcept; NXSelectionWrapper& operator=(NXSelectionWrapper&& other) noexcept; // 初始化选择(可能抛异常) void initSelection(const char* cue, UF_UI_selection_p_t filter); private: bool m_isActive{false}; }; // 主逻辑函数声明(使用错误码接口) extern "C" int safe_create_cylinder_entry();

5.2 源文件实现(safe_create_cylinder.cpp)

#include “safe_create_cylinder.h” #include <stdexcept> #include <sstream> // --- NXSelectionWrapper 实现 --- NXSelectionWrapper::~NXSelectionWrapper() noexcept { if (m_isActive) { try { // 终止选择,恢复之前的模式。UF_UI_reset_prototype必须成功。 UF_UI_reset_prototype(); } catch (...) { // 析构函数必须吞掉所有异常,防止异常逃逸 UF_UI_write_listing_window(“警告:重置选择原型时发生异常。”); } m_isActive = false; } } void NXSelectionWrapper::initSelection(const char* cue, UF_UI_selection_p_t filter) { int status = UF_UI_select_with_single_dialog(cue, filter, nullptr, nullptr, nullptr, nullptr); if (status != 0) { throw std::runtime_error(“初始化选择对话框失败”); } m_isActive = true; } // --- 业务逻辑函数 --- namespace { // 内部函数:获取圆柱面数据(可能抛异常) struct CylinderData { double radius; double height; double origin[3]; double direction[3]; }; CylinderData getCylinderDataFromFace(tag_t faceTag) { CylinderData data{}; // 使用NX API查询面信息。这些API返回错误码,我们需要检查。 int type, subtype; int status = UF_MODL_ask_face_type(faceTag, &type, &subtype); if (status != 0 || type != UF_MODL_CYLINDRICAL_FACE) { throw std::runtime_error(“所选面不是圆柱面。”); } // 获取圆柱面参数 status = UF_MODL_ask_face_parms(faceTag, data.origin, data.direction, &data.radius, &data.height); if (status != 0) { throw std::runtime_error(“无法获取圆柱面参数。”); } return data; } // 内部函数:创建新圆柱体(返回错误码) int createNewCylinder(const CylinderData& data, tag_t* newBodyTag) { if (newBodyTag == nullptr) return UF_FAIL; uf_list_p_t edge_list = nullptr; double limits[2] = {0.0, data.height}; double origin[3], axis[3]; memcpy(origin, data.origin, 3 * sizeof(double)); memcpy(axis, data.direction, 3 * sizeof(double)); UF_FEATURE_SIGN sign = UF_NULLSIGN; int status = UF_MODL_create_cylinder1(sign, data.radius, origin, axis, limits, &edge_list, newBodyTag); // 清理边列表(如果创建了) if (edge_list != nullptr) { UF_MODL_delete_list(&edge_list); } return status; } } // namespace anonymous // --- 暴露给NX的命令入口函数 --- extern “C” int safe_create_cylinder_entry() { // 步骤1:初始化返回码和RAII对象 int finalReturnCode = 1; // 默认失败 NXSelectionWrapper selectionGuard; // RAII对象,确保选择被清理 try { // 步骤2:提示用户选择圆柱面 UF_UI_selection_p_t filter = nullptr; UF_UI_set_sel_mask(filter, UF_UI_SEL_MASK_FEATURE_FACE); // 只允许选择面 selectionGuard.initSelection(“请选择一个圆柱面”, filter); // 步骤3:执行选择循环(简化,实际需处理回调) // 这里假设通过其他方式(如UF_UI_select_with_single_dialog)已获得选择结果tag_t tag_t selectedFaceTag = ...; // 从选择响应中获取 if (selectedFaceTag == NULL_TAG) { UF_UI_write_listing_window(“未选择面或选择已取消。”); finalReturnCode = 0; // 用户取消不算错误 return finalReturnCode; } // 步骤4:获取数据(可能抛异常) CylinderData cylData; try { cylData = getCylinderDataFromFace(selectedFaceTag); } catch (const std::exception& e) { UF_UI_write_listing_window(e.what()); // 获取数据失败,返回错误码1 return finalReturnCode; } // 步骤5:创建新圆柱体(使用错误码) tag_t newBodyTag = NULL_TAG; int createStatus = createNewCylinder(cylData, &newBodyTag); if (createStatus != 0) { UF_UI_write_listing_window(“创建新圆柱体失败。”); // 创建失败,但资源(selectionGuard)会被自动清理 return finalReturnCode; } // 步骤6:成功 UF_UI_write_listing_window(“同心圆柱体创建成功!”); finalReturnCode = 0; // 成功 } catch (const std::exception& e) { // 捕获所有在try块中抛出的、未被内部catch处理的标准异常 UF_UI_write_listing_window(std::string(“程序发生异常: “) + e.what()); // finalReturnCode 保持为1(失败) } catch (...) { UF_UI_write_listing_window(“发生未知异常。”); // finalReturnCode 保持为1(失败) } // 步骤7:函数返回,selectionGuard等所有栈上RAII对象自动析构,清理资源 return finalReturnCode; }

5.3 要点解析与避坑指南

  1. RAII贯穿始终NXSelectionWrapper确保了无论函数正常返回还是异常退出,NX的选择模式都会被正确重置,避免了界面状态混乱。
  2. 清晰的错误处理边界getCylinderDataFromFace内部使用异常,因为它进行的是数据验证和转换的逻辑操作。createNewCylinder使用NX错误码,因为它直接调用NX API。顶层入口函数safe_create_cylinder_entry捕获所有异常,并将其转化为用户可读的信息和一致的返回码。
  3. 资源释放:注意createNewCylinder函数中,无论创建成功与否,都尝试清理edge_list。这是基本的资源管理,即使在错误路径上也不能忘记。
  4. 用户取消的处理:用户取消选择不被视为错误,返回0,这是一种良好的用户体验设计。

6. 高级话题与调试技巧

6.1 构造函数与析构函数中的异常

这是一个进阶但重要的话题。简单规则是:

  • 构造函数:如果构造函数无法完成对象的构建,抛出异常是合适的。这确保了不会存在一个“半成品”对象。
  • 析构函数绝对不要让异常从析构函数中逃逸。如果析构函数中调用的操作可能抛异常(如某些NX删除操作),必须用try-catch(...)块捕获并吞掉(或记录日志)。因为当栈因异常展开而调用析构函数时,如果析构函数再抛异常,C++运行时通常会直接调用std::terminate终止程序。

6.2 使用标准库容器与算法

C++标准库(如std::vector,std::string,std::map)的绝大多数操作都提供了强异常保证或至少基本保证。例如,std::vector::push_back在发生异常(如元素拷贝构造函数抛出)时,向量本身的状态保持不变(强保证)。充分利用这些特性,可以大大简化你的异常安全代码。

6.3 调试“NX捕获到标准C++异常”

当这个错误出现时,按以下步骤排查:

  1. 检查入口点封装:确认你的ufusrufsta函数是否被一个顶层的try-catch(...)块包裹。这是最基本的防线。
  2. 缩小范围:如果入口点已封装,说明异常是在catch(...)块内部抛出的!这通常发生在:
    • 析构函数:在栈展开过程中,某个RAII对象的析构函数又抛出了异常。
    • catch块内的代码:你在catch块中调用了某个可能失败的操作(如记录日志的NX API调用失败并抛异常?实际上NX API不抛异常,但如果你用了其他C++库可能会)。
  3. 使用调试器:在Visual Studio等IDE中,可以设置调试器在“抛出C++异常时”中断。这样,当异常第一次被抛出时,程序就会暂停,你能看到完整的调用栈,精准定位到抛出异常的代码行。
  4. 审查资源管理类:重点检查所有自定义的RAII类(管理NX标签、内存、文件等)的析构函数,确保它们noexcept或内部妥善处理了所有异常。
  5. 审查全局和静态对象:全局或静态对象的构造和析构也可能抛异常,且难以捕获。在NX插件中,尽量避免使用复杂的全局对象。

7. 总结与最佳实践清单

掌握NX下的C++异常安全编程,是一个从“被动处理崩溃”到“主动设计健壮性”的思维转变。最后,我将最关键的经验提炼成一份可快速查阅的清单:

  1. 入口点必须设防:所有NX回调函数和入口点,用try-catch(...)包裹,防止任何异常逃逸到NX内核。
  2. 拥抱RAII:为所有需要手动管理的资源(NX对象标签、内存、选择会话、文件句柄)创建RAII包装类。让析构函数负责释放。
  3. 析构函数必须不抛异常:确保所有RAII类的析构函数不会抛出异常。内部调用可能失败的操作时,使用try-catch(...)并记录日志。
  4. 明确错误处理策略:在模块内部使用异常进行错误传递(逻辑清晰),在模块边界(尤其是调用大量NX API的函数)将异常转换为错误码(兼容NX框架)。
  5. 善用标准库:优先使用std::unique_ptr,std::shared_ptr,std::vector,std::string等提供强异常保证的组件。
  6. 拷贝-交换实现强保证:对于需要原子性修改的关键数据,使用“拷贝-交换”惯用法来提供强异常保证。
  7. 谨慎对待全局状态:避免使用全局变量,特别是那些构造函数和析构函数可能抛出异常或依赖复杂初始化的对象。如果必须使用,考虑使用“首次使用时构造”的单例模式(注意线程安全)。
  8. 测试异常路径:不仅测试正常流程,还要有意识地测试错误路径。模拟内存分配失败、NX API调用失败等情况,验证你的代码是否能正确清理并报告错误。

编写异常安全的代码起初会感觉有些束缚,需要多思考。但一旦形成习惯,你会发现它带来的好处是巨大的:更少的崩溃、更易维护的代码、以及用户对你插件稳定性的信任。在NX这样高价值的设计环境中,这份信任至关重要。