代码之家  ›  专栏  ›  技术社区  ›  Josh Kelley

C++中的异常调度

  •  3
  • Josh Kelley  · 技术社区  · 15 年前

    应如何调度异常,以便以集中、用户友好的方式处理错误处理和诊断?

    例如:

    • datahw类处理与某些数据采集硬件的通信。
    • datahw类可能会根据一些可能的错误引发异常:间歇性信号、无信号、CRC故障、驱动程序错误。每种类型的错误都有自己的异常类。
    • datahw类由许多不同的代码片段调用,这些代码执行不同类型的采集和分析。

    正确的错误处理策略取决于异常类型和尝试的操作。(对于间歇信号,重试x次,然后告诉用户;对于驱动程序错误,记录错误并重新启动驱动程序;等等)应如何调用此错误处理策略?

    • 将错误恢复编码到每个异常类中:这将导致异常类非常大,并且包含高级UI和系统管理代码。这似乎不好。
    • 提供单独的 catch 针对每种异常类型的块:由于从许多不同的位置调用datahw类,因此每个 抓住 块必须在每个调用站点重复。这似乎不好。
    • 使用单一 抓住 阻止调用某些 ExceptionDispatch 基于巨型RTTI的函数 switch 声明:RTTI和 转换 通常表示应用OO设计失败,但这似乎是最不坏的选择。
    4 回复  |  直到 15 年前
        1
  •  6
  •   fizzer    15 年前

    通过捕获(…)并调用一个共享处理程序函数来重新调用和分派,避免在每个调用站点复制catch块:

    f()
    {
        try
        {
            // something
        }
        catch (...)
        {
            handle();
        }
    }
    
    void handle()
    {
        try
        {
            throw;
        }
        catch (const Foo& e)
        {
            // handle Foo
        }
        catch (const Bar& e)
        {
            // handle Bar
        }
        // etc
    }
    
        2
  •  2
  •   aib    15 年前

    我经常遇到的一个想法是,异常应该被能够处理它们的级别捕获。例如,传输数据的函数可能会捕捉到CRC错误,一旦捕捉到该异常,它可能会尝试重新传输,而“无信号”异常可能会捕捉到更高级别的异常,并中断或延迟整个操作。

    但我的猜测是,这些异常中的大多数都会被捕获在同一个函数周围。它 单独捕获和处理它们是个好主意(如soln 2),但您认为这会导致大量重复代码(导致soln 3)。

    我的问题是,如果有很多代码要重复,为什么不把它变成一个函数呢?

    我在考虑…

    void SendData(DataHW* data, Destination *dest)
    {
        try {
            data->send(dest);
        } catch (CRCError) {
            //log error
    
            //retransmit:
            data->send(dest);
        } catch (UnrecoverableError) {
            throw GivingUp;
        }
    }
    

    我想应该是exceptiondispatch()函数,而不是 switch 在异常类型上,它将包装异常生成调用本身,并且 catch 例外情况。

    当然,这个函数过于简化了—您可能需要围绕datahw使用一个完整的包装器类;但我的观点是,最好有一个集中的点来处理所有datahw异常—如果该类的不同用户处理它们的方式相似的话。

        3
  •  1
  •   Iocio    15 年前

    也许您可以为datahw类编写一个包装类? 包装器将提供与datahw类相同的功能,但也包含所需的错误处理代码。好处是,您可以将错误处理代码放在一个地方(干燥原则),所有错误都将被统一处理。例如,可以将包装器中的所有低级I/O异常转换为高级异常。 基本上防止向用户显示低级异常。

    正如巴特勒·兰普森所说:计算机科学中的所有问题都可以通过另一个间接层次来解决。

        4
  •  1
  •   Johannes Schaub - litb    15 年前

    有三种方法可以解决这个问题。

    正在写入包装函数

    为每个函数编写一个包装函数,该函数可以引发处理异常的异常。然后,所有调用方调用该包装器,而不是原始的抛出函数。

    使用函数对象

    另一种解决方案是采用更通用的方法,编写一个函数,它接受一个函数对象并处理所有异常。下面是一些例子:

    class DataHW {
    public:
        template<typename Function>
        bool executeAndHandle(Function f) {
            for(int tries = 0; ; tries++) {
                try {
                    f(this);
                    return true;
                }
                catch(CrcError & e) {
                    // handle crc error
                }
                catch(IntermittentSignalError & e) {
                    // handle intermittent signal
                    if(tries < 3) {
                        continue;
                    } else {
                        logError("Signal interruption after 3 tries.");
                    } 
                }
                catch(DriverError & e) {
                    // restart
                }
                return false;
            }
        }
    
        void sendData(char const *data, std::size_t len);
        void readData(char *data, std::size_t len);
    };
    

    现在,如果你想做点什么,你可以做:

    void doit() {
        char buf[] = "hello world";
        hw.executeAndHandle(boost::bind(&DataHW::sendData, _1, buf, sizeof buf));
    }
    

    因为您提供了函数对象,所以也可以管理状态。假设senddata更新len,这样它就知道读取了多少字节。然后,您可以编写读取和写入的函数对象,并为到目前为止读取的字符数保持计数。

    第二种方法的缺点是不能访问抛出函数的结果值,因为它们是从函数对象包装器调用的。无法轻松获取函数对象绑定器的结果类型。一种解决方法是编写一个结果函数对象,该对象在函数对象执行成功后由ExecuteAndHandle调用。但是,如果我们把太多的工作放在第二种方法上,仅仅是为了让所有的家政工作顺利进行,那么结果就不值得了。

    两者结合

    还有第三种选择。我们可以将这两个解决方案(包装器和函数对象)结合起来。

    class DataHW {
    public:
        template<typename R, typename Function>
        R executeAndHandle(Function f) {
            for(int tries = 0; ; tries++) {
                try {
                    return f(this);
                }
                catch(CrcError & e) {
                    // handle crc error
                }
                catch(IntermittentSignalError & e) {
                    // handle intermittent signal
                    if(tries < 3) {
                        continue;
                    } else {
                        logError("Signal interruption after 3 tries.");
                    } 
                }
                catch(DriverError & e) {
                    // restart
                }
                // return a sensible default. for bool, that's false. for other integer
                // types, it's zero.
                return R();
            }
        }
    
        T sendData(char const *data, std::size_t len) {
            return executeAndHandle<T>(
                boost::bind(&DataHW::doSendData, _1, data, len));
        }
    
        // say it returns something for this example
        T doSendData(char const *data, std::size_t len);
        T doReadData(char *data, std::size_t len);
    };
    

    诀窍是 return f(); 模式。即使F返回空值,我们也可以返回。这最终是我最喜欢的,因为它既允许将句柄代码集中在一个地方,也允许在包装函数中进行特殊处理。您可以决定是否最好将其拆分,并创建一个拥有该错误处理程序函数和包装器的类。可能这是一个更清洁的解决方案(我想 Separation of Concerns 在这里。一个是基本的数据硬件功能,另一个是错误处理)。