繁体   English   中英

在C ++中调度异常

[英]Dispatching exceptions in C++

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

例如:

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

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

  • 将错误恢复编码到每个异常类中:这将导致异常类相当大并包含高级UI和系统管理代码。 这看起来很糟糕。
  • 为每种类型的异常提供单独的catch块:由于DataHW类是从许多不同的地方调用的,因此每个调用站点都必须复制每个catch块。 这看起来很糟糕。
  • 使用单个catch块,使用基于RTTI的巨型switch语句调用一些ExceptionDispatch函数:RTTI和switch通常表示无法应用OO设计,但这似乎是最不好的选择。

通过捕获(...)并调用重新抛出和分派的共享处理函数,避免在每个调用站点复制catch块:

f()
{
    try
    {
        // something
    }
    catch (...)
    {
        handle();
    }
}

void handle()
{
    try
    {
        throw;
    }
    catch (const Foo& e)
    {
        // handle Foo
    }
    catch (const Bar& e)
    {
        // handle Bar
    }
    // etc
}

我一直在考虑的一个想法是异常应该由可以处理它们的级别捕获。 例如,CRC错误可能被传输数据的函数捕获,并且在捕获此异常时,它可能会尝试重新传输,而“无信号”异常可能会被捕获到更高级别并丢弃或延迟整个操作。

但我的猜测是,大多数这些异常都将围绕同一个函数捕获。 单独捕获和处理它们个好主意(如soln#2),但是你说这会导致很多重复的代码(导致解决#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异常 - 如果类的不同用户处理它们的方式是相似的。

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

编写包装函数

为每个函数编写一个包装函数,它可以抛出异常来处理异常。 然后,所有调用者调用该包装器,而不是原始投掷函数。

使用函数对象

另一种解决方案是采用更通用的方法并编写一个函数来获取函数对象并处理所有异常。 这是一些例子:

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,以便它知道读取了多少字节。 然后,您可以编写读取和写入的函数对象,并维护到目前为止读取的字符数。

第二种方法的缺点是您无法访问throw函数的结果值,因为它们是从函数对象包装器调用的。 没有简单的方法来获取函数对象绑定器的结果类型。 一种解决方法是在执行函数对象成功后编写由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返回void,我们也可以返回。 这最终将成为我的最爱,因为它允许两者将句柄代码集中在一个地方,但也允许在包装函数中进行特殊处理。 您可以决定是否最好将其拆分并创建一个具有该错误处理函数和包装器的自己的类。 可能这将是一个更清洁的解决方案(我想到这里的分离关注 。一个是基本的DataHW功能,一个是错误处理)。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM