【问题标题】:Clarification on member function template specialization using enable_if使用 enable_if 澄清成员函数模板特化
【发布时间】:2019-04-07 12:30:36
【问题描述】:

我想了解在尽量减少成员函数模板专业化的冗长程度时我哪里出错了。我在相当随意地这样做时会遇到编译错误。这是有效的版本,希望能对我想要实现的目标有所启发:

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

基本上我正在尝试创建一个运行时变量维向量类,当用指针实例化时,它可以用作对相同类型向量的一种引用(更准确地说,我有每个坐标的几个数组一组点,我想使用“参考”向量来处理这些点,就好像它们在内存中以相反的方式排序一样)。 然而,当我尝试通过删除我认为不必要的typename U 来简化代码时。我在 MSVC2017 中收到以下编译错误:std::enable_if_t&lt;false,void&gt;' : 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 中也被注释掉)。我想了解是什么导致第二个代码无法编译。

我不直接专门化该类的原因是,在我的原始实现中,我有很多其他成员函数,它们在两个专门化之间是等效的,我不想重复。

【问题讨论】:

  • 您是否知道,如果您将复制构造函数模板化,它就不再是复制构造函数了? eel.is/c++draft/class.copy.ctor#1
  • 无论如何,我相信你可以在这里找到一些相关信息:std::enable_if to conditionally compile a member function。根据那里的答案,第三种情况下的 SFINAE 也不能正常工作。
  • 实际上我认为模板应该使复制 ctor 只是一个自定义 ctor,但后来 MSVC 吐槽说我已经定义了两个复制 ctor,然后我把它弄丢了。基本上我不知道发生了什么,我通常会按照我期望直观地工作,但最后 5 次我决定让模板更多地涉及到,它以我报告的编译器中的错误结束 5 次我相信他们在某个时候修复了 MS,所以此时我只是问其他人它是否按预期工作。
  • "更准确地说,我有一组点的每个坐标的几个数组,我想使用“参考”向量来处理这些点,就好像它们是被订购的一样反过来在内存中"你知道reverse iterator的概念吗?
  • 另外,请注意,与 typedef 的名称可能暗示的相反,i32 不能保证是 32 位整数。是的,它恰好在 MSVC 上。但是,如果您的代码的正确性确实取决于具有保证的固定位数的整数,那么为什么不直接使用&lt;cstdint&gt; 中提供的固定宽度整数类型呢? (注意:我真的不明白为什么用于表示rtvec 大小的精确位数在这里是必不可少的)

标签: c++ templates sfinae member-functions enable-if


【解决方案1】:

但是,当我尝试通过删除我认为不必要的内容来简化代码时

很遗憾(如果我理解正确的话)你删除了一些必要的东西

如果我理解正确,您已经简化了以下方法

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

在方法的模板参数中“转换”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];
}

【讨论】:

  • 我认为这可能是问题所在,这就是我实际想出可编译代码的方式。就 nullptr 而言,我实际上在这里和eli.thegreenplace.net/2014/sfinae-and-enable_if 上提到了许多答案。你能否详细说明为什么你认为它不是一个合法的构造(或者它会破坏哪个编译器)?您还可以澄清为什么我的第三个代码似乎可以工作,只要注释掉的行保持注释掉,但即使其中一个存在也不行?为什么会这样,对我来说似乎有点违反直觉。整个问题实际上源于
  • ...由于缺少对函数模板部分特化的支持。但是,是的,我尝试使用 SFINAE 解决它。
  • @lightxbulb 关于void* 非类型模板参数的(il)合法性参见,例如:Why using 0 as default non type template parameter for void* is not allowed 或pointer to void as template non-type parameter。
  • @DanielLangr 所以它是不一致的,但主要编译器支持它?很高兴知道,从现在开始我会尝试使用 bool。
  • @lightxbulb - 确切地说:从未见过不支持它的编译器(在发现这个问题之前我已经使用了无数次)但是,阅读标准,似乎不符合标准。关于您的第三个代码可以评论该功能的第二个版本这一事实,我想这是因为 SFINAE 不起作用,功能已启用,当您只有一个功能有效时,两个具有相同签名的功能您有冲突而且编译的不知道用的是哪个版本。
【解决方案2】:

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

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

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&lt;T*&gt;,它将返回对 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
         }
     }
}   

【讨论】:

  • Uhmmm... 不:SFINAE 表单 typename = std::enable_if_t&lt;someTest&gt; 可以使用,但最好使用 std::enable_if_t&lt;someTest, int&gt; = 0 表单,因为当您想编写相同方法的两个替代版本时,只有第二个可以使用。跨度>
  • Consexpr 似乎是一个不错的选择,感谢您的推荐!我相信我的 SFINAE 用法应该是正确的,我参考了很多关于此的帖子。
  • 啊 nvm,constexpr 不能与 at 函数一起使用,因为返回类型不同,我认为上述内容也不能作为复制构造函数。
猜你喜欢
  • 2013-08-07
  • 2012-01-22
  • 2012-10-01
  • 1970-01-01
  • 2012-04-03
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
相关资源
最近更新 更多