【问题标题】:How to handle an exception in a clean way in C++如何在 C++ 中以干净的方式处理异常
【发布时间】:2013-02-10 09:45:04
【问题描述】:

我的问题是我正在编写一个应该在未来可读的程序,并且该程序有很多异常情况。所以每当我必须抛出异常时,我必须编写超过 10 行来初始化我的异常类并将程序中的信息附加到它。例如如下:

MyExceptionClass ex;
ex.setErrorMessage("PIN_CANNOT_GO_IN");
ex.setErrorDetails("The pin is asked to go to the state IN while the depth of the r-coordinate does not support it");
ex.setResolutionMessage("Configure your coordinates-file to move first to the correct position before changing the state of the pin");
ex.VariableList().resize(5);
ex.VariableList()[0].push_back("Pin state: ");
ex.VariableList()[0].push_back(ToString(pin.getPinState()));
ex.VariableList()[1].push_back("Pin target state: ");
ex.VariableList()[1].push_back(ToString(coordinatesData[coordinatesIndex].targetPinState));
ex.VariableList()[2].push_back("Current r Value: ");
ex.VariableList()[2].push_back(ToString(EncoderPosition.r));
ex.VariableList()[3].push_back("Current phi Value: ");
ex.VariableList()[3].push_back(ToString(EncoderPosition.phi));
ex.VariableList()[4].push_back("Current z Value: ");
ex.VariableList()[4].push_back(ToString(EncoderPosition.z));

ex.printLog();
ex.writeLog(exceptionLogFilePath.getValue());

throw ex;

所以只有 5 个变量,我必须写下所有这些...... 有没有一种有效的方法来包含程序中的所有信息(至少是变量)​​,而不是每次我想抛出异常时都重写所有这些信息?

提前致谢。

【问题讨论】:

  • 首先,用构造函数构造对象,而不是一堆setter。这就是构造函数的用途。其次,无论如何,你的例外是一堆字符串,只需将它们格式化为一个大的用户可呈现的大字符串并触发。
  • 您是否尝试过编写一个生成异常的函数(即,包含除throw 行之外的所有内容),并且只执行throw exceptionGenerator() 或者您如何调用该函数?
  • 函数如何访问我程序中的变量列表?这意味着我将不得不通过参数传递它们,然后我们回到第一方。有没有办法自动将所有变量添加到我的异常类中?

标签: c++ exception coding-style readability throw


【解决方案1】:

您可以使用一个通用函数 (fill_out_exception_parameters) 填充通用异常的 VariableList 对象,并在您编写的任何新异常类中重新使用它

【讨论】:

  • 函数如何访问我程序中的变量列表?这意味着我将不得不通过参数传递它们,然后我们回到第一方。有没有办法自动将所有变量添加到我的异常类中?
【解决方案2】:

如果添加到异常类的数据仅用于显示错误消息,您可以使用字符串连接来减少使用的push_back() 的数量。

例如,您可以使用:

ex.VariableList()[0].push_back(string("Pin state: ") + ToString(pin.getPinState());

您甚至可以连接所有其他消息,而不是为每个消息使用单独的索引(1、2、3、4 等)。

此外,对于每个字段,您可以使用专用的 setter 方法来提供适当的值。例如:

ex.VariableList()[0].setPinState(ToString(pin.getPinState()));

然后应该将"Pin state: " 部分移动到错误消息的位置打印


更进一步,您的异常类可以有一个专用方法,该方法接受所有导致错误消息的对象,并改为调用该消息。例如:

void MyExceptionClass::setMessage(Pin& pin, CoordinatesData& cd, EncoderPosition& ep) {
    setPinState(ToString(pin.getPinState()));
    // set whatever else you want here
}

此外,将ToString() 部分移动到要打印消息的任何位置,只需将值存储在异常类中即可。例如,将上面的行更改为(您需要相应地更改签名):

setPinState(pin.getPinState());

并让打印逻辑决定如何将其转换为字符串。另一个优点是它允许您以不同的格式打印相同的消息。

【讨论】:

  • 感谢您的想法。但我仍在寻找一种更简洁的方法,可以自动添加所有变量,我所要做的就是编写错误消息,并期望程序自动处理相同的变量名和多个点的当前值程序,如果你明白我的意思的话。
  • @SamerAfach 我认为我更新的答案非常接近您想要的。玩一下,让我知道这是否有帮助。 :)
【解决方案3】:

您可以使用 Boost Exception 简化向异常对象添加任意数据的流程,并在它们在调用堆栈中冒泡时使用更多相关数据来扩充它们。您不必担心预先定义可能需要存储在异常中的所有内容,因为您可以根据需要存储任何异常数据。

【讨论】:

    【解决方案4】:

    我认为我有最干净的方法来做到这一点。请让我听听你的想法。

    所以我将所有相关变量封装在一个模板化类中,如下所示(只是一个简单粗暴的例子)

    class VarBase
    {
    VarBase();
    static std::vector<VarBase*> __allParams;
    string getStringValue() = 0;
    };
    
    template <typename T>
    class Var : public VarBase
    {
        T value;
        string name;
        string description;
        toString();
        operator T();
        string getStringValue();
    };
    
    VarBase::VarBase()
    {
        __allParams.push_back(this);
    }
    VarBase::~VarBase()
    {
        //handle removing from __allParams vector or whatever container
    }
    template <typename T>
    std::string Var<T>::getStringValue()
    {
        std::stringstream s;
        s << paramValue;
        return s.str();
    }
    

    现在,如果我的异常类是 VarBase 类的朋友,它可以访问 __allParams 并循环访问它并调用 getStringValue(),它会自动将值转换为字符串并在必要时将其添加到我的异常类中:)

    非常感谢任何其他想法。

    【讨论】:

    • 对方法有意见吗?
    • @MatthieuM。感谢您的回复。为什么它不是线程安全的?向量 __allParams 仅在创建变量或引发异常时才需要访问,并且您可以使用映射来存储具有唯一 ID 的点。为什么它不是自动化的?异常类将读取所有存储的指针,当程序崩溃时,它可以从中获取所有可用信息。对?请解释你的观点。
    • 它不是线程安全的,因为单个向量在没有显式同步的线程之间共享(这是没有线程安全的教科书定义)。关于自动化,使用Var&lt;T&gt; 的唯一方法是将其用作局部变量(否则复制/赋值会妨碍);因为您可能想要记录不是Var&lt;T&gt; 的参数,所以我推测它的目的是捕获引用;虽然可能是错误的(并且受到 的方式的影响)。
    【解决方案5】:

    我遇到了类似的问题:如何丰富异常的上下文信息?

    Boost 提出了一种解决方案:try/catch 并在重新抛出之前丰富 catch 块中的异常。它确实丰富了异常,但不是自动

    我最终想出的解决方案非常简单,并且基于 C++ 析构函数的强度。但首先,如何使用它:

    void foo(int i) {
        LOG_EX_VAR(i);
        // do something that might throw
    }
    

    是的,就是这样,一个宏调用和i 连同宏展开的函数名、文件名和行号一起被添加到异常上下文中。

    背后是什么? most important const 和一点魔法。

    class LogVar {
    public:
        LogVar(LogVar const&) = delete;
        LogVar& operator=(LogVar const&) = delete;
    
        virtual ~LogVar() {}
    
    protected:
        LogVar();
    }; // class LogVar
    
    template <typename T>
    class LogVarT: public LogVar {
    public:
        LogVarT(char const* fc, char const* fl, int l, char const* n, T const& t):
            _function(fc), _filename(fl), _line(l), _name(n), _t(t) {}
    
        ~LogVar() {
            ContextInterface::AddVariable(_function, _filename, _line, _name, _t);
        }
    
    private:
        char const* _function;
        char const* _filename;
        int _line;
    
        char const* _name;
        T const& _t;
    }; // class LogVarT
    
    template <typename T>
    LogVarT make_log_var(char const* fc,
                         char const* fl,
                         int l,
                         char const* n,
                         T const& t)
    {
        return LogVarT(fc, fl, l, n, t);
    }
    
    #define LOG_EX_VAR(Var_)                                                      \
        LogVar const& BOOST_PP_CAT(_5416454614, Var_) =                           \
            make_log_var(__func__, __FILE__, __LINE__, #Var_, Var_);
    

    如果您可以让困难的部分(ContextInterface::AddVariable() 函数)工作,这会相当有效。

    如果您不想打扰它,请像您一样选择thread_local std::vector&lt;LogVar*&gt;。请注意,您将白做很多工作。

    如果你对它感兴趣,那就继续。

    1. 让我们意识到这里最重要的部分是获得线程安全的东西。所以上下文将是全局的......每个线程(又名thread_local)。但即便如此,人们也可能不小心泄露了对它的引用。
    2. 重要的是要认识到几个异常可能并存,尽管在任何时间点只有一个未被捕获:即一个异常可能会在 catch 子句中引发。
    3. 我们只能检测我们自己抛出的异常,因此我们需要为其他异常制定某种默认策略。例如,日志记录。

    那么,让我们把界面搞清楚:

    class ContextInterface {
    public:
        typedef std::unique_ptr<ContextInterface> UPtr;
        typedef std::shared_ptr<ContextInterface> SPtr;
        typedef std::weak_ptr<ContextInterface> WPtr;
    
        static UPtr SetDefault(UPtr d) {
            std::swap(d, DefaultContext);
            return d;
        }
    
        template <typename T, typename... Args>
        static SPtr SetActive(Args&&... args) {
            SPtr ci = ExceptionContext.lock();
            if (ci.get()) { return ci; }
    
            ci.reset(new T(std::forward<Args>(args)...));
            ExceptionContext = ci;
            return ci;
        }
    
        template <typename T>
        static void AddVariable(char const* fc,
                                char const* fl,
                                int l,
                                char const* n,
                                T const& t)
        {
            SPtr sp = ExceptionContext.lock();
            ContextInterface* ci = sp.get();
    
            if (not ci) { ci = DefaultContext.get(); }
    
            if (not ci) { return; }
    
            ci->report(fc, fl, l, n) << t;
        }
    
        virtual ~ContextInterface() {}
    
    private:
        static thread_local UPtr DefaultContext;
        static thread_local WPtr ExceptionContext;
    
        virtual std::ostream& report(char const* fc,
                                     char const* fl,
                                     int l,
                                     char const* n) = 0;
    }; // class ContextInterface
    

    最后,最后一个缺失的部分(好吧,除了我猜你想要的实际上下文):基本异常类的示例。

    class ContextualException: public virtual std::exception {
    public:
        ContextualException(): _c(ContextInterface::SetActive<ExceptionContext>()) {}
    
        ContextInterface const& context() const { return *_c; }
    
    private:
        ContextInterface::SPtr _c;
    }; // class ContextualException
    

    【讨论】:

    • 我必须对你的方法进行更深入的思考。
    • @SamerAfach:慢慢来,特别是因为我不太确定后面部分(AddVariable 实现)。
    • 感谢您的代码。让我看看我是否正确理解了您的代码的目的。所以你完全按照我的方法,但唯一的区别是你在共享部分上引入了某种互斥锁?在我的示例中,我不能只为 __allParams 使用自制的互斥锁吗?
    • @SamerAfach:实际上,我并没有完全使用你的方法。 1/ 异常是线程本地的(在某种程度上),因此与其他线程共享__allParams(顺便说一句,保留标识符)没有意义;应该是thread_local 2/ 你的方法是悲观的(我猜),因为你总是在__allParams 中注册东西,不管它们是否真的有用。我的方法仅在由异常引起的堆栈展开期间在“上下文”中注册事物。因此,无异常执行(几乎)不会受到惩罚......
    • @SamerAfach: [cont] 3/ 你的方法似乎注册了 VarBase 的实例,而且只有那些。我的做法是让你实例化你想要的任何东西,然后要求你明确地识别那些你想出现在异常堆栈转储中的变量。
    【解决方案6】:

    这些文本描述应该与异常类固有地相关联,而不是作为运行时数据写入每个实例。

    同样,所有信息数据都应该是异常类的成员,您可以将其格式化以便稍后输出为文本(可能在异常类本身的成员函数中)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-04-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多