Enable class constructor in some enumerated template cases

Viewed 179

For performance reasons, I'm using a templated class with an enum instead of relay on inheritance (It is not an option).

At this point I have something like:

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
}

Now I'm using an annoying amount of ifs/switches over T and rising exceptions if an incompatible constructor is called for a defined T.

What I want is to use std::enable_if or equivalent to prevent throwing exceptions on constructor and detect on compilation time such kind of errors.

I've tried many stack-overflows and foreign sites std::enable_if examples, but I can barely understand what I'm really doing and I always end on a compilation error.

Thanks in advance and sorry for asking for a probably trivially answered question. I'm noob with templates.

Environment: Linux GCC 8 and c++14 Restrictions: Maximal performance with no virtual methods.

3 Answers

What I want is to use std::enable_if or equivalent to prevent throwing exceptions on constructor and detect on compilation time such kind of errors.

I've tried many stack-overflows and foreign sites std::enable_if examples, but I can barely understand what I'm really doing and I always end on a compilation error.

The problem with std::enable_if (and SFINAE, more in general) is that it works only checking template parameters. So can enable/disable a full class, with a test over a template parameter of the class, but can't enable/disable a single method, with a test over a template parameter of the class.

If you want SFINAE enable/disable a method (like your constructors) you have to made it a template method and test a template parameter of the method itself.

So you can't write something as

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

because T is a template parameter of the class, not of the constructor.

But there is a trick: you can use default values/types for template parameters; so the following code works

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

because is checked the value U that is a template parameter of the constructor.

To enable the second constructor only when T is B or C, you can write

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

The following is a full compiling example

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

Now I'm using an annoying amount of ifs/switches over T and rising exceptions if an incompatible constructor is called for a defined T.

So it seems you don't need your constructor to be SFINAE friendly, so static_assert seems sufficient:

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, "!");
        // ...
    }
    //...
};

Static asserts are fine - but you might delete these constructors for all enum values - except the one you want to provide:

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

};

Now - explicit enabling:

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) {}

One big advantage over sulution with static_assert is that you can check if Queue is constructed from the given set of parameters (so you can make further 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

Related