2

假设我有这个基类:

class Foo public: std::exception
{
    public:
        // printf()-style parms are formatted with vsnprintf() into a message
        Foo( const char * format, ... );
        // lots more stuff in this class not relevant to this example
};

现在我需要在几十个地方使用它作为基础异常类。然后可以编写 catch() 子句以仅捕获基类或某些需要的派生类。

我想做的是保持几十个新的类定义非常简单。我正在阅读 C++11 中的继承构造函数,我认为需要一些类似的东西:

class A : public Foo { public: using Foo::Foo; };
class B : public Foo { public: using Foo::Foo; };

问题是这个项目也是在 Windows 中使用 Microsoft Visual Studio 编译的,据我所知,它不支持继承构造函数

是否有另一种明显的方法可以保持派生类相对简单?也许有模板?在模板方面,我是新手,如果这是正确的做法,我可以朝正确的方向使用。

4

3 回答 3

3

像这样的东西怎么样:

class Base : public std::exception {};

template <int UNUSED>
class Foo : public Base
{
    public:
        // printf()-style parms are formatted with vsnprintf() into a message
        Foo( const char * format, ... );
        // lots more stuff in this class not relevant to this example
};

typedef Foo<1> A;
typedef Foo<2> B;

编辑添加一个基类,以便能够捕获所有 Foo 异常。

于 2013-10-12T03:40:18.813 回答
1

继承构造函数的要点是继承许多构造函数,而您的示例只有一个。除此之外……</p>

是的,当继承构造函数不起作用时,有一个惯用的解决方法,原因是支持不佳或者可能只是它们所做的一些怪癖。您可以改用完美转发。

template< typename ... a >
A( a && ... arg ) : Foo( std::forward< a >( arg ) ... ) {}

缺点是转换是在Foo( … )调用站点应用的,而不是在A( … )调用站点。因此,转发构造函数将优于需要转换的可能更合适的构造函数。并且花括号初始化列表不能是模板推导的参数,而它们将适用于继承构造函数。而且你不能对多个基地这样做,尽管这种情况很少发生。

这应该适用于 C 风格的可变参数列表或其他任何东西……尽管取决于您的编译器版本,在这种极端情况下完美转发也可能是一个棘手的问题。

于 2013-10-12T01:51:11.433 回答
1

我可以想到几种不同的方法:

  1. 使用可变参数模板或模拟它们。
  2. 使用带有 mixin 类型的模板。
  3. 制作Foo一个模板类。
  4. 避免使用 C 风格的可变参数构造函数。

这里有一个剧透:我建议避免使用 C 风格的可变参数构造函数。对于想要快速回答的人来说,只需阅读下面的这一点就足够了。

可变参数模板

代码看起来像这样

class A : virtual public Foo
{
public:
    template <typename Ts>
    explicit A( Ts&&...ts ) : Foo( std::forward<Ts>(ts)... ) {}
};

所有A::A接收到的参数都简单地转发给 的构造函数Foo。实际上它与继承构造函数相同。不幸的是,VS10 和 VS11 都不支持可变参数模板。但是有一种方法可以模仿:

class A : virtual public Foo
{
public:
    template <typename T1>
    explicit A( T1 && t1 ) 
     : Foo( std::forward<T1>(t1)
     ) {}
    template <typename T1
            , typename T2>
    A( T1 && t1
     , T2 && t2 ) 
     : Foo( std::forward<T1>(t1)
          , std::forward<T2>(t2) 
     ) {}
    template <typename T1
            , typename T2
            , typename T3>
    A( T1 && t1
     , T2 && t2
     , T3 && t3 ) 
     : Foo( std::forward<T1>(t1)
          , std::forward<T2>(t2) 
          , std::forward<T3>(t3) 
     ) {}
    // and so forth, until a certain limit
};

我知道这很丑陋。但即使是标准库实现也使用这种臃肿的技术。可能,您不想为您编写的每个异常类都这样做。相反,您可以使用为您执行实现的模板类来执行此操作,正如我现在将展示的那样。

具有混合类型的模板

为了避免上面每个类的代码膨胀,您可以让模板类为您完成工作:

template <typename Mixin>
class FooImpl : virtual public Foo, public Mixin
{
public:
    template <typename Ts>
    explicit FooImpl( Ts&&...ts ) 
        : Foo( std::forward<Ts>(ts)... ), Mixin() {}        
};

您将类的新功能A放入一个混合类中AMixin,然后您可以编写

typedef FooImpl<AMixin> A;

并且您获得了所需的功能。当然,由于您没有可变参数模板,因此您必须使用一些臃肿的代码。但这只是一次。此外,如果您愿意,可以让混合类虚拟继承自Foo,如果您需要该类功能:

class AMixin : virtual public Foo
{
    AMixin() : Foo( "" ) {}
    // new functionality
};

虚拟继承在这里有一个很好的副作用,即最派生的类决定Foo调用哪个构造函数。在我们的例子中,这将是FooImpl模板类。因此,不必担心Foo( "" ).

制作Foo模板类

另一种方法是制作Foo一个执行实现的模板类。

template <typename Tag>
class Foo : public std::exception
{
public:
    Foo( const char * s, ... );
    // other functionality
};

typedef Foo<struct ATag> A;
typedef Foo<struct BTag> B;

这种方法对混合方法有几个缺点:

  • 你们没有共同的基础。您无法捕获Foo异常,而只能捕获特定的派生类。
  • 您不能使用混合类向派生类添加功能。

第二点可能不是那么糟糕,因为如果您愿意,您可以扩展您的 Foo 类以涵盖功能。对于异常类的用户来说,类型通常是足够的信息。对于第一点,有一个解决方案。Foo为继承的模板类创建一个公共基类std::exception

class AbstractFoo : public std::exception
{
public:
    // other functionality from above, 
    // possibly some pure virtual functions.
    // constructors will be generated by the compiler.
};

template <typename Tag>
class Foo : public AbstractFoo
{
public:
    Foo( const char * s, ... );
    // other functionality
};

typedef Foo<struct ATag> A;
typedef Foo<struct BTag> B;

现在客户端代码可以捕获AbstractFoo模板实例化的公共基础。请注意,在这种情况下,客户端必须通过引用来捕获。这是一件好事,因为这是正确执行此操作的方法。(否则,你会遇到类型切片的麻烦。)

避免 C 风格的可变参数构造函数

C 风格的可变参数函数不是类型安全的。尤其是在通常是测试最少的错误处理代码中,这是要避免的,因为它容易出错(你会得到运行时错误而不是编译时错误)。因此,更喜欢产生编译时错误并避免这些可变参数构造函数的编程技术。与其传递可变数量的参数,不如只传递一个std::string. 调用者可以轻松地将字符串放在一起:

// Your code
class Foo : public std::exception
{
    Foo( std::string message );
    // other stuff
};

// client code
if ( error )
    throw Foo( "Could not open file '" + fileName + "'." );

派生类可以简单地将它们的std::string参数转发给它们的基类Foo"%s"没有我个人觉得丑陋的 C 样式格式列表,客户端代码看起来干净简单。可能您对这种方法有一些反对意见:

  • 如果我想打印一些数字怎么办?那么,使用std::to_string(i).
  • 如果在构造字符串的过程中抛出异常怎么办?这是可能的,但极不可能。可以抛出什么样的异常?可能是一个std::bad_alloc?我想不出别的了。对于一些字符串的构造,有几个字节要在堆上分配。不太可能抛出异常。但是想一想:这也可能发生在可变参数的实现中,即使你不使用任何可能扔进你的类的东西。当客户端代码尝试抛出异常时,异常会在概念上被复制到某个未定义的位置(根据 C++ 标准)。这个未定义的地方可能是堆(当然它不是堆栈)并且在这种情况下可能会耗尽内存 - 你猜对了 - 一个std::bad_alloc被抛出而不是你的异常。因此,您无法避免在异常创建或复制期间在客户端代码中引发异常的可能性。

我的建议:只需使用std::stringas 构造函数参数即可。这就是工作方式std::runtime_errorstd::runtime_error您也可以考虑从而不是直接派生std::exception,因为它有意义地实现了该what()功能。

于 2013-10-12T12:56:51.087 回答