我可以想到几种不同的方法:
- 使用可变参数模板或模拟它们。
- 使用带有 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 类以涵盖功能。对于异常类的用户来说,类型通常是足够的信息。对于第一点,有一个解决方案。为继承std::exception的Foo模板类创建一个通用基类:
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 作为构造函数参数即可。这就是std::runtime_error 的工作方式。您也可以考虑直接从std::runtime_error 派生而不是std::exception,因为它有意义地实现了what() 函数。