Search code examples
c++templatessfinaemember-functionsenable-if

Clarification on member function template specialization using enable_if


I would like to understand where I am going wrong in trying to minimize the verbosity of my member functions template specialization. I get compilation errors when doing so rather arbitrarily. Here's the version that works, which hopefully will shed some light on what I am trying to achieve:

#include <iostream>
#include <type_traits>


typedef int i32;
template<class T>
struct rtvec
{
private:
    T* e;
    i32 d;

public:
    rtvec(i32 d) : d(d), e(new T[d]) {}
    //template<typename Args...>
    //rtvec()
    rtvec(const rtvec& in) : d(in.d), e(new T[in.d])
    {
        for (i32 i = 0; i < d; ++i)
            at(i) = in.at(i);
    }

    rtvec(rtvec<typename std::remove_pointer_t<T>>& in) : d(in.dim()), e(new T[in.dim()])
    {
        for (i32 i = 0; i < d; ++i)
            e[i] = &in.at(i);
    }
    ~rtvec() { delete[] e; }
    i32 dim() const { return d; }

    template<typename U=T, 
        typename std::enable_if_t<std::is_same_v<U,T>>* = nullptr,
        typename std::enable_if_t<!std::is_pointer_v<U>>* = nullptr>
    inline T& at(i32 i) 
    {
        return e[i];
    }

    template<typename U = T,
        typename std::enable_if_t<std::is_same_v<U, T>>* = nullptr,
        typename std::enable_if_t<std::is_pointer_v<U>>* = nullptr>
        inline typename std::remove_pointer_t<T>& at(i32 i)
    {
        return *e[i];
    }

};


int main()
{
    rtvec<float> v(2);
    v.at(0) = 1;
    v.at(1) = 2;
    rtvec<float*> p = v;
    p.at(0) = 5;
    std::cout << v.at(0) << " " << v.at(1) << "\n";
    return 0;
}

Basically I am trying to make a runtime variable dimensional vector class, which when instantiated with a pointer, can be used as a sort of reference to a vector of the same type (more precisely I have several arrays of each of the coordinates of a set of points and I want to use the "reference" vector to be able to work with those as if they were ordered the other way around in memory). When I try to simplify the code, however, by trying to remove what I perceive as an unnecessary typename U. I get the following compilation error in MSVC2017: std::enable_if_t<false,void>' : Failed to specialize alias template. Here's the less verbose version that I was aiming at achieving:

struct rtvec
{
private:
    T* e;
    i32 d;

public:
    rtvec(i32 d) : d(d), e(new T[d]) {}

    template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
    rtvec(const rtvec& in) : d(in.d), e(new T[in.d])
    {
        for (i32 i = 0; i < d; ++i)
            at(i) = in.at(i);
    }

    template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
    rtvec(rtvec<typename std::remove_pointer_t<T>>& in) : d(in.dim()), e(new T[in.dim()])
    {
        for (i32 i = 0; i < d; ++i)
            e[i] = &in.at(i);
    }
    ~rtvec() { delete[] e; }
    i32 dim() const { return d; }


    template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
    inline T& at(i32 i)
    {
        return e[i];
    }

    template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
    inline typename std::remove_pointer<T>::type& at(i32 i)
    {
        return *e[i];
    }

};

If I modify this a little, however, it does compile:

template<class T>
struct rtvec
{
private:
    T* e;
    i32 d;

public:
    rtvec(i32 d) : d(d), e(new T[d]) {}

    template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
    rtvec(const rtvec& in) : d(in.d), e(new T[in.d])
    {
        for (i32 i = 0; i < d; ++i)
            at(i) = in.at(i);
    }

    /*template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
    rtvec(rtvec<typename std::remove_pointer_t<T>>& in) : d(in.dim()), e(new T[in.dim()])
    {
        for (i32 i = 0; i < d; ++i)
            e[i] = &in.at(i);
    }*/
    ~rtvec() { delete[] e; }
    i32 dim() const { return d; }


    template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
    inline T& at(i32 i)
    {
        return e[i];
    }

    /*template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
    inline typename std::remove_pointer<T>::type& at(i32 i)
    {
        return *e[i];
    }*/


};

(as long as the part relating to the pointer is commented out in main too). I want to understand what is it that makes the 2nd code not compile.

The reason I am not directly specializing the class is that in my original implementation I have a lot of other member functions that will be equivalent between the two specializations which I do not want to repeat.


Solution

  • When I try to simplify the code, however, by trying to remove what I perceive as an unnecessary

    Unfortunately (if I understand correctly) you have removed something that is necessary

    If I understand correctly, you have simplified the following methods

    template<typename U=T, 
        typename std::enable_if_t<std::is_same_v<U,T>>* = nullptr,
        typename std::enable_if_t<!std::is_pointer_v<U>>* = nullptr>
    inline T& at(i32 i) 
    {
        return e[i];
    }
    
    template<typename U = T,
        typename std::enable_if_t<std::is_same_v<U, T>>* = nullptr,
        typename std::enable_if_t<std::is_pointer_v<U>>* = nullptr>
        inline typename std::remove_pointer_t<T>& at(i32 i)
    {
        return *e[i];
    }
    

    as follows

    template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
    inline T& at(i32 i)
    {
        return e[i];
    }
    
    template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
    inline typename std::remove_pointer<T>::type& at(i32 i)
    {
        return *e[i];
    }
    

    Unfortunately SFINAE, over a template method, works only when uses tests (std::enable_if_t) based on template parameters of the method itself.

    I mean: SFINAE doesn't works when the std::enable_if_t tests involve T (and only T) because T is a template parameter of the struct, not a template parameter of the method.

    So you need the trick

    typename U = T
    

    that "transform" the T type in a template parameter of the method.

    You can simplify a little removing the typename before std::enable_if_t because the "_t" is exactly typename (see the definition of std::enable_if_t)

    Off topic: I'm not a language layer but, as far I know,

     std::enable_if_t<std::is_same_v<U,T>>* = nullptr
    

    isn't perfectly legal; I suggest to rewrite using int instead of void *

     std::enable_if_t<std::is_same_v<U,T>, int> = 0
    

    or maybe bool

     std::enable_if_t<std::is_same_v<U,T>, bool> = true
    

    or other integer types

    Concluding, I suggest to rewrite your at() method as follows

    template <typename U = T, 
              std::enable_if_t<std::is_same_v<U, T>, int> = 0,
              std::enable_if_t<!std::is_pointer_v<U>, int> = 0>
    inline T& at(i32 i) 
    {
        return e[i];
    }
    
    template<typename U = T,
             std::enable_if_t<std::is_same_v<U, T>, int> = 0,
             std::enable_if_t<std::is_pointer_v<U>, int> = 0>
        inline typename std::remove_pointer_t<T>& at(i32 i)
    {
        return *e[i];
    }