lightxbulb
lightxbulb

Reputation: 1321

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.

Upvotes: 0

Views: 976

Answers (2)

max66
max66

Reputation: 66200

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

Upvotes: 5

mohabouje
mohabouje

Reputation: 4050

There are several reasons and you need to provide the full compilation error. Regarding your code, you seem to use C++17.

For instance, in this code you are trying to return a reference to the object stored in the array, right?:

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

Can be replaced with a more STL-Like code:

using reference = T&;
using const_reference = const T&;

reference at(i32 i) {
    return e[i];
}

const_reference at(i32 i) const {
    return e[i];
}

Or use auto:

auto at(i32 i) const {
    return e[i];
}

This is the way most of the STL containers work. For instance, if you access an std::vector<T*>, it will return a reference to a T*, not a reference to the data where T points.

Regarding the SFINAE technique you are using, I am not sure if it's properly written.

For instance, take a look into this post to find information about the proper ways to write the conditions for selecting constructors. Small sumary:

template <typename = typename std::enable_if<... condition...>::type>
explicit MyAwesomeClass(MyAwesomeClass<otherN> const &);

For instance, if you want to enable a constructor only for those instance that do not hold a pointer type:

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

Now, regarding the fact that you are using C++17, you can us constexpr if that will make your life much easy and handle the different situations. Something like this, I guess:

template <typename U>
explicit rtvec(const rtvec<U>& in) : d(in.d), e(new T[in.d]) {    
     for (i32 i = 0; i < d; ++i){
         if constexpr (std::is_pointer<T>::value &&
                       std::is_pointer<U>::value) {
             // ...
         } else if constexpr (!std::is_pointer<T>::value &&
                       std::is_pointer<U>::value) {
             // ...
         } else {
             //  rest of possible combinations
         }
     }
}   

Upvotes: 1

Related Questions