使用 enable_if 澄清成员函数模板特化

Clarification on member function template specialization using enable_if

我想了解我在尝试尽量减少成员函数模板专业化的冗长性时哪里出错了。随意这样做时我会遇到编译错误。这是有效的版本,希望它能阐明我正在努力实现的目标:

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

基本上我正在尝试制作一个运行时变量维度向量 class,当用指针实例化时,它可以用作对相同类型向量的一种引用(更准确地说我有几个一组点的每个坐标的数组,我想使用 "reference" 向量来处理它们,就像它们在内存中以相反的方式排序一样)。 然而,当我试图通过删除我认为不必要的东西来简化代码时 typename U。我在 MSVC2017 中收到以下编译错误:std::enable_if_t<false,void>' : Failed to specialize alias template。这是我旨在实现的更简洁的版本:

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

};

不过,如果我稍微修改一下,它确实可以编译:

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


};

(只要与指针相关的部分也在main中被注释掉)。我想了解是什么导致第二个代码无法编译。

我不直接特化 class 的原因是,在我最初的实现中,我有很多其他成员函数,它们在我不想重复的两个特化之间是等价的。

有多种原因,您需要提供完整的编译错误。关于您的代码,您似乎使用 C++17。

例如,在这段代码中,您试图 return 对存储在数组中的对象的引用,对吗?:

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

可以用更像 STL 的代码替换:

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

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

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

或使用auto:

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

这是大多数 STL 容器的工作方式。例如,如果您访问 std::vector<T*>,它将 return 对 T* 的引用,而不是对 T 指向的数据的引用。

关于您使用的SFINAE技术,我不确定它是否写得正确。

例如,查看此 post 以查找有关编写选择构造函数条件的正确方法的信息。小总结:

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

例如,如果您只想为那些不包含指针类型的实例启用构造函数:

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

现在,关于您使用的是 C++17,您可以 constexpr if 这将使您的生活变得更加轻松并处理不同的情况。我猜是这样的:

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

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

不幸的是(如果我理解正确的话)你删除了一些必要的东西

如果我没理解错,你已经简化了下面的方法

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

如下

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

不幸的是,SFINAE 在模板方法上仅在使用基于方法本身的模板参数的测试 (std::enable_if_t) 时有效。

我的意思是:当 std::enable_if_t 测试涉及 T(并且仅涉及 T)时,SFINAE 不起作用,因为 T 是结构的模板参数,不是方法的模板参数。

所以你需要技巧

typename U = T

"transform" T 键入方法的模板参数。

您可以稍微简化一下,删除 std::enable_if_t 之前的 typename,因为“_t”正好是 typename(参见 std::enable_if_t 的定义)

题外话:我不是语言层,但据我所知,

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

不完全合法;我建议使用 int 而不是 void *

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

或者 bool

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

或其他整数类型

最后,我建议重写你的at()方法如下

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