【问题标题】:Enable class constructor in some enumerated template cases在某些枚举模板案例中启用类构造函数
【发布时间】:2018-11-06 15:36:35
【问题描述】:

出于性能原因,我使用带有枚举的模板类而不是继承继承(这不是一个选项)。

此时我有类似的东西:

typedef enum { A, B, C, D } QueueType;

template <QueueType T> class Queue {
    Queue(int a){...} // only usable when T = A
    Queue(unsigned a, unsigned b){...} // only usable when T = B || T = C
    Queue(somestruct z){...} // only usable when T = B || T = C
    //other constructors
}

现在,如果为已定义的 T 调用不兼容的构造函数,我会使用大量令人讨厌的 ifs/switches 来处理 T 并引发异常。

我想要的是使用std::enable_if 或等效项来防止在构造函数上抛出异常并在编译时检测此类错误。

我尝试了许多堆栈溢出和外国网站std::enable_if 示例,但我几乎无法理解我真正在做什么,并且总是以编译错误结束。

在此先感谢您提出一个可能回答得很琐碎的问题。我是模板的菜鸟。

环境:Linux GCC 8 和 c++14 限制:没有虚拟方法的最大性能。

【问题讨论】:

  • 你能把你的尝试和你得到的错误一起展示吗?
  • Queue(int a, std::enable_if_t&lt;T == A&gt;* = nullptr){...} // only usable when T = A 呢?你试过了吗?
  • @PiotrNycz ,不完全是,因为我一直尝试将它放在模板上而不是作为参数。
  • 是什么阻止您使用继承?因为如果仅仅是继承以某种方式使事情变慢的想法,您可能会发现编译器比您认为的要聪明。喜欢以直截了当的方式写东西。你未来的自己会感谢你。
  • @AndreKostur 我必须以每秒数百万的速度处理数据包。每个 if/branch 都很重要,即使缓存/分支预测是如何做出的。如果你必须考虑虚方法并解决它们,在这种情况下就太简单了。当然,这并不是说继承不好或类似的东西,而且,我们在其他地方使用它们,但是在关键性能点上,仅仅几个内存访问或分支就可以让我们损失一些 gigabit/s/per core跨度>

标签: c++ templates c++14 sfinae enable-if


【解决方案1】:

我想要的是使用 std::enable_if 或等效项来防止在构造函数上抛出异常并在编译时检测此类错误。

我尝试了许多堆栈溢出和外国网站std::enable_if 示例,但我几乎无法理解我真正在做什么,并且总是以编译错误结束。

std::enable_if(和 SFINAE,更一般地说)的问题在于它只能检查模板参数。所以可以启用/禁用一个完整的类,测试类的模板参数,但不能启用/禁用单个方法,测试类的模板参数。

如果您希望 SFINAE 启用/禁用一个方法(如您的构造函数),您必须将其设为模板方法并测试该方法本身的模板参数。

所以你不能写成这样

template <typename = std::enable_if_t<T == A>>
Queue (int)
 { } // only usable when T = A

因为T 是类的模板参数,而不是构造函数的模板参数。

但是有一个技巧:您可以使用模板参数的默认值/类型;所以下面的代码有效

template <QueueType U = T, typename = std::enable_if_t<U == A>>
Queue (int)
 { } // only usable when T = A 

因为检查了值U,它是构造函数的模板参数。

仅当TBC 时才启用第二个构造函数,可以这样写

template <QueueType U = T, typename = std::enable_if_t<(U == B) || (U == C)>> 
Queue (unsigned, unsigned)
 { } // only usable when T = B || T = C

以下是完整的编译示例

#include <type_traits>

typedef enum { A, B, C, D } QueueType;

template <QueueType T>
struct Queue
 {
   template <QueueType U = T, typename = std::enable_if_t<U == A>>
   Queue (int)
    { } // only usable when T = A

   template <QueueType U = T, typename = std::enable_if_t<(U == B) || (U == C)>>
   Queue (unsigned, unsigned)
    { } // only usable when T = B || T = C
 };

int main()
 {
   Queue<A>  qa0{1};         // compile
   //Queue<A>  qa1{1u, 2u};  // compilation error

   // Queue<B>  qb0{1};      // compilation error
   Queue<B>  qb1{1u, 2u};    // compile

   // Queue<C>  qc0{1};      // compilation error
   Queue<C>  qc1{1u, 2u};    // compile

   // Queue<D>  qd0{1};      // compilation error
   // Queue<D>  qd1{1u, 2u}; // compilation error
 }

【讨论】:

  • 非常感谢@max66。我终于让它工作了,我想我也明白它是如何工作的。
【解决方案2】:

现在,如果为已定义的 T 调用不兼容的构造函数,我会使用大量的 ifs/switches 和不断上升的异常。

看来你不需要你的构造函数对 SFINAE 友好,所以static_assert 似乎就足够了:

template <QueueType T>
class Queue {
public:
    Queue(int a)
    {
        static_assert(T == A, "!");
        // ...
    }
    Queue(unsigned a, unsigned b)
    {
        static_assert(T == B || T == C, "!");
        // ...
    }
    Queue(somestruct z)
    {
        static_assert(T == B || T == C, "!");
        // ...
    }
    //...
};

【讨论】:

  • 但请确保添加比"!"更有用的错误消息。
  • @G.Sliepen 编译器通常会显示错误所在的行,因此恕我直言,在这种特殊情况下最好不要写消息
  • 我同意没有比"!" 更好的消息,但我还是会写消息。否则,如果有人使用了错误的构造函数,他们将得到类似file.cc:123 error: static assertion failed: static_assert(T == A) 的错误,这不是很有帮助,尤其是如果使用错误构造函数的人对class Queue 的内部一无所知,并且可能不知道甚至不知道它是一个模板。暗示存在一个 不同的 构造函数并且必须使用它会很好。
  • @G.Sliepen:C++11/C++14 需要消息,只有从 C++17 开始,我们才能放置消息:-/。我不怀疑可以添加有意义的信息,但我这里没有相关信息。
【解决方案3】:

静态断言很好——但你可以删除所有枚举值的这些构造函数——除了你想提供的那个:

template <QueueType T> 
class Queue 
{
public:    
    Queue(int a) = delete; // only usable when T = A
    //other constructors
    Queue(unsigned a, unsigned b) = delete; // only usable when T = B || T = C
    Queue(somestruct z) = delete; // only usable when T = B || T = C

private:
    // not necessary - but allows to have a little less code
    struct EnablerType {};
    static constexpr EnablerType Enabler{};
    Queue(unsigned a, unsigned b, EnablerType) { }// only usable when T = B || T = C
    Queue(somestruct z, EnablerType) { } // only usable when T = B || T = C

};

现在 - 显式启用:

template <>
inline Queue<A>::Queue(int a) {}
template <>
inline Queue<B>::Queue(unsigned a, unsigned b) : Queue(a, b, Enabler) {}
template <>
inline Queue<C>::Queue(unsigned a, unsigned b) : Queue(a, b, Enabler) {}
template <>
inline Queue<B>::Queue(somestruct z) : Queue(z, Enabler) {}
template <>
inline Queue<C>::Queue(somestruct z) : Queue(z, Enabler) {}

static_assert sulution 相比的一大优势是,您可以检查Queue 是否是根据给定的一组参数构造的(因此您可以进行进一步的 SFINAE):

int main() {
    static_assert(std::is_constructible_v<Queue<A>, int>, "failed");
    static_assert(!std::is_constructible_v<Queue<B>, int>, "failed");
    ...
}

Live demo

【讨论】:

  • 缺点是错误信息类似于error: use of deleted function ‘Queue&lt;T&gt;::Queue(int)'static_assert() 将允许出现更有用的错误消息,如果构造函数一开始没有被删除,编译器(好吧,我在这里只测试了 GCC)会列出可能的其他构造函数。
  • @G.Sliepen 我为我的答案添加了一个优于 static_assert 解决方案的优势
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多