在C ++中使用模板或继承构造函数

时间:2013-10-12 00:25:06

标签: class templates c++11

说我有这个基类:

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; };

问题是这个项目也使用Microsoft Visual Studio在Windows中编译,据我所知it doesn't support inheriting constructors

还有另一种明显的方法可以保持派生类相对简单吗?也许有模板?对于模板而言,我是新手,如果这是正确的方法,我可以使用正确的方向。

3 个答案:

答案 0 :(得分: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异常。

答案 1 :(得分:1)

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

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

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

缺点是转化应用于Foo( … )呼叫网站,而不是A( … )呼叫网站。因此,转发构造函数优于可能更合适的需要转换的构造函数。并且braced-init-lists不能是模板推导的参数,而它们将用于继承构造函数。你不能为多个基地做这件事,尽管这种情况很少发生。

这应该适用于C风格的可变参数列表或其他任何内容......虽然根据您的编译器版本,在这种极端情况下的完美转发也可能是一个棘手的问题。

答案 2 :(得分:1)

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

  1. 使用可变参数模板或模拟它们。
  2. 使用带有mixin类型的模板。
  3. Foo成为模板类。
  4. 避免使用C风格的可变参数构造函数。
  5. 这是一个剧透:我建议避免使用C风格的可变参数构造函数。对于想要快速回答的人来说,只需阅读下面的内容即可。

    Variadic模板

    代码看起来像这样

    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的{​​{1}}模板类创建公共基类:

    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; ,这是模板实例化的通用基础。请注意,在这种情况下,客户端必须通过引用捕获。这是一件好事,因为这是正确的方法。 (否则,你会遇到类型切片问题。)

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

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

    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的C风格格式列表,我个人觉得这很难看。可能你对这种方法有些反对意见:

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

    我的建议:只需要std::bad_alloc作为构造函数参数。这就是std::string的工作方式。您也可以考虑直接从std::runtime_error而不是std::runtime_error派生,因为它有意义地实现了std::exception函数。