【问题标题】:Using templates or inheriting constructors in C++在 C++ 中使用模板或继承构造函数
【发布时间】:2013-10-12 00:25:05
【问题描述】:

假设我有这个基类:

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 编译的,据我所知it doesn't support inheriting constructors

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

【问题讨论】:

  • 这些类是否仅在名称上有所不同,例如您的示例中的AB(这对于异常类型可能有意义),或者它们是否包含其他通常不存在的差异影响构造函数?
  • 为什么不能只指定从 A 或 B 调用 Foos 构造函数 - 你不应该直接调用 Foos 构造函数
  • 我的意思是从A和B的构造函数中,你可以调用Foos的构造函数
  • 因为构造函数使用 ... 来指定可变数量的参数,我不能轻易地从 A 或 B 中“复制”该构造函数。如果不是这种情况,请发布一个示例,说明如何这样做,因为我不知道怎么做。
  • @BenS 正确,没有其他区别。

标签: class templates c++11


【解决方案1】:

这样的事情怎么样:

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 异常。

【讨论】:

  • 这有什么用?是的,如果他需要很多功能相同的类型,而 Base 中没有任何功能,它应该会很好用。
  • @Potatoswatter 这正是他所要求的。
  • 这些是异常类。他们唯一不同的是他们的名字。这就是异常几乎总是发生的方式。 OP 的要求之一是能够捕获所有Foo 异常。 Base 的 catch 块在我的方案中实现了这一点。
  • @Potatoswatter Foo&lt;1&gt;Foo&lt;2&gt; 不是同一类型。
  • 异常类通常不会实现太多额外的功能,但是将自己完全归零似乎并不是那么好的架构。至少,您可能需要某种层次结构。
【解决方案2】:

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

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

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

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

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

【讨论】:

  • 在我的代码中,我碰巧有很多构造函数,而不仅仅是我在这个例子中列出的 1 个。这就是我试图简化派生类的原因。我将阅读完美转发。感谢您提供代码示例。
【解决方案3】:

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

  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 类以涵盖功能。对于异常类的用户来说,类型通常是足够的信息。对于第一点,有一个解决方案。为继承std::exceptionFoo模板类创建一个通用基类:

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() 函数。

【讨论】:

  • 可变模板构造函数很好。 C 风格的可变参数列表是不安全的。这在底部的部分之前可能会更清楚。
  • 好主意。我会改的。
猜你喜欢
  • 2013-05-21
  • 1970-01-01
  • 1970-01-01
  • 2013-09-04
  • 2018-09-29
  • 2012-04-18
  • 2019-12-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多