我可以想到几种不同的方法:
- 使用可变参数模板或模拟它们。
- 使用带有 mixin 类型的模板。
- 制作
Foo
一个模板类。
- 避免使用 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::string
as 构造函数参数即可。这就是工作方式std::runtime_error
。std::runtime_error
您也可以考虑从而不是直接派生std::exception
,因为它有意义地实现了该what()
功能。