使用 std::enable_if<> 进行模板特化

3
以下代码可以编译并运行:
#include <cinttypes>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>

class UnsignedBox {
public:
    typedef std::uint64_t box_type;

    template<typename UNSIGNED_TYPE, 
        typename std::enable_if<
        std::numeric_limits<UNSIGNED_TYPE>::is_signed==false &&
        (sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type)), int>::type = 0 
    >
    UNSIGNED_TYPE toUnsigned()const {
        //We've established we're not returning a smaller type so we can just 
       //return our value.
        return value;
    }

    template<typename UNSIGNED_TYPE, 
       typename std::enable_if<std::numeric_limits<UNSIGNED_TYPE>::is_signed==false &&
       (sizeof(UNSIGNED_TYPE) < sizeof(UnsignedBox::box_type)), int>::type = 0
    >
    UNSIGNED_TYPE toUnsigned()const {
        //We are returning  a smaller type so we need a range check.
        if(value>static_cast<box_type>(std::numeric_limits<UNSIGNED_TYPE>::max())){
            std::ostringstream msg;
            msg<<value<<'>'<<
               static_cast<box_type>(std::numeric_limits<UNSIGNED_TYPE>::max());
            throw std::logic_error(msg.str());
        }
        return value;
    }

    UnsignedBox(const box_type ivalue): value(ivalue){}
private:
    box_type value;

};

int main(int argc, char*argv[]) {

    UnsignedBox box(
        static_cast<UnsignedBox::box_type>(    
           std::numeric_limits<std::uint32_t>::max())+10
        );

    std::uint64_t v(box.toUnsigned<std::uint64_t>());
    std::cout<<v<<std::endl;

    try {
        std::uint32_t v(box.toUnsigned<std::uint32_t>());
    }catch(const std::logic_error err){
        std::cout<<err.what()<<std::endl;
    }

    return EXIT_SUCCESS;
}

预期输出(所有支持的平台):
4294967305
4294967305>4294967295

到目前为止都很顺利。

但是我真正想做的(为了代码清晰):

是声明类似于:

template<typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned()const;

接下来提供专业的实现,例如:

template<typename UNSIGNED_TYPE, 
        typename std::enable_if<
        std::numeric_limits<UNSIGNED_TYPE>::is_signed==false &&
        (sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type)), int>::type = 0 
    >
    UNSIGNED_TYPE UnsignedBox::toUnsigned()const {
        //We've established we're not returning a smaller type so we can just 
       //return our value.
        return value;
    }


    template<typename UNSIGNED_TYPE, 
       typename std::enable_if<std::numeric_limits<UNSIGNED_TYPE>::is_signed==false &&
       (sizeof(UNSIGNED_TYPE) < sizeof(UnsignedBox::box_type)), int>::type = 0
    >
    UNSIGNED_TYPE UnsignedBox::toUnsigned()const {
        //We are returning  a smaller type so we need a range check.
        if(value>static_cast<box_type>(std::numeric_limits<UNSIGNED_TYPE>::max())){
            std::ostringstream msg;
            msg<<value<<'>'<<
               static_cast<box_type>(std::numeric_limits<UNSIGNED_TYPE>::max());
            throw std::logic_error(msg.str());
        }
        return value;
    }

但是我遇到了这个错误:
xxx.cpp:nn:20: error: prototype for 'UNSIGNED_TYPE UnsignedBox::toUnsigned() const' does not match any in class 'UnsignedBox'
      UNSIGNED_TYPE UnsignedBox::toUnsigned()const {
                    ^ xxx.cpp:nn:23: error: candidate is: template<class UNSIGNED_TYPE> UNSIGNED_TYPE UnsignedBox::toUnsigned() const
         UNSIGNED_TYPE toUnsigned()const;
                       ^

这很奇怪,因为如果你问我原型的定义,

UNSIGNED_TYPE UnsignedBox::toUnsigned() const

对于IT技术而言,这是一个非常合适的组合。

UNSIGNED_TYPE toUnsigned()const;

我做错了什么?

附言:这不是实际问题,但我的问题类似,我想基于编译时检查的原始类型属性来特殊处理一些模板。


1
旁注:还有一个std::is_unsigned特性,可能比使用std::numeric_limits类更合适。 - davidhigh
2个回答

3

您不能使用一个签名声明函数:

template<typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned() const;

然后使用不同的签名来定义它:

template<typename UNSIGNED_TYPE, 
    typename std::enable_if<
    std::numeric_limits<UNSIGNED_TYPE>::is_signed==false &&
    (sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type)), int>::type = 0 
>
UNSIGNED_TYPE UnsignedBox::toUnsigned() const;

第一个函数接受一个模板参数,第二个函数接受两个参数,即使其中一个有默认值。这两个参数必须完全匹配。所以您需要 两个 声明:

template <typename UNSIGNED_TYPE,
          typename = typename std::enable_if<
                        std::is_unsigned<UNSIGNED_TYPE>::value &&
                        sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type)
                        >::type>
UNSIGNED_TYPE toUnsigned() const;

template <typename UNSIGNED_TYPE,
          typename = typename std::enable_if<
                        std::is_unsigned<UNSIGNED_TYPE>::value &&
                        sizeof(UNSIGNED_TYPE) < sizeof(UnsignedBox::box_type)
                        >::type>
UNSIGNED_TYPE toUnsigned() const;

接下来是两个定义。但这本身并不起作用,因为我们实际上正在重新定义默认的模板参数,所以您需要在返回类型上进行SFINAE,例如:

template <typename UNSIGNED_TYPE>
typename std::enable_if<
    std::is_unsigned<UNSIGNED_TYPE>::value &&
    sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type),
    UNSIGNED_TYPE>::type
toUnsigned() const;


template <typename UNSIGNED_TYPE>
typename std::enable_if<
    std::is_unsigned<UNSIGNED_TYPE>::value &&
    sizeof(UNSIGNED_TYPE) < sizeof(UnsignedBox::box_type),
    UNSIGNED_TYPE>::type
toUnsigned() const;

尽管有一个更简单的方法是只使用一个 toUnsigned() 函数,根据 sizeof 调用两个不同的成员函数:
template <typename UNSIGNED_TYPE,
          typename = typename std::enable_if<std::is_unsigned<UNSIGNED_TYPE>::value>::type>
UNSIGNED_TYPE toUnsigned() const {
    return toUnsigned<UNSIGNED_TYPE>(
        std::integral_constant<bool, 
            (sizeof(UNSIGNED_TYPE) >= sizeof(UnsignedBox::box_type))>{});
}

template <typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned(std::true_type /* bigger */);

template <typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned(std::false_type /* smaller */);

@DanAllen 答案是你的声明和定义必须匹配。现在它们不匹配。enable_if 类型占位符必须出现在两个位置上。因此,要么使用 enable_if 声明并只用 <typename UNSIGNED, typename> 定义它,要么反过来。另外,你会遇到相同签名的问题,所以最好采用分派方法。 - Barry
硬币开始掉落了。当我包含2个声明并提供2个实现时,它可以工作。我知道这一点。我想要一个声明来驱动类声明中有多少实现的详细信息。然而,事实证明,这些声明不仅需要在语义上匹配,还需要在词法上匹配!如果我甚至将std::numeric_limits<UNSIGNED_TYPE>::is_signed==false更改为std::numeric_limits<UNSIGNED_TYPE>::is_signed==(1==0),它会抱怨原型。你知道我可以阅读关于模板声明如何被视为等效的任何地方吗? - Persixty
1
@DanAllen C++标准中的“函数模板重载”([temp.over.link])部分。简短的回答是,它不必在词法上匹配,模板参数名称允许不同。但基本上就是这样。 - Barry
谢谢。我从未想过它不会在实例化时递归解析模板并从那里开始!我还确定您可以使用限定和非限定名称,因此(如果您之前说过using namespace std;,则enable_ifstd :: enable_if是相同的。但是,等价性非常狭窄。幸运的是,我已经成功地实现了我想要的(将该垃圾从我的漂亮类声明中移出到实现中),使用部分类特化。我稍后会发布我的答案。 - Persixty
谢谢。这已经足够让我思考,以产生完整的答案,达到我想要的“卫生”因素。实际上,我认为这是以不那么多的模板花招为代价的。我会说肯定比std::enable_if<>少一点,这是一个有点恶心的hack tbt。(我的答案如下)。 - Persixty

0

在Barry(上面)的一些见解之后,我已经确定了我的答案应该是什么:

#include <cinttypes>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>


//Here is the real aim - a short and sweet class declaration pretty much free
//of implementation junk and jiggery-pokery.

class UnsignedBox {
public:
    typedef std::uint64_t box_type;

    template<typename UNSIGNED_TYPE> UNSIGNED_TYPE toUnsigned()const;

    UnsignedBox(const box_type ivalue): value(ivalue){}
private:
    box_type value;
};

//Now things get a bit more verbose...

namespace UnsignedBox_support {

    template<
        typename FROM_TYPE,
        typename TO_TYPE,
        bool IS_UNSIGNED=(std::numeric_limits<TO_TYPE>::is_signed==false),
        bool FROM_IS_LARGER=(sizeof(FROM_TYPE)>sizeof(TO_TYPE))
    >
    class ToUnsigned{ };

    template<typename FROM_TYPE,typename TO_TYPE>
    class ToUnsigned<FROM_TYPE,TO_TYPE,true,false>{
        template<typename UNSIGNED_TYPE> 
            friend UNSIGNED_TYPE UnsignedBox::toUnsigned()const;

        static TO_TYPE convert(const FROM_TYPE v){
            //No checking...
            return static_cast<TO_TYPE>(v);
        }
    };

    template<typename FROM_TYPE,typename TO_TYPE>
    class ToUnsigned<FROM_TYPE,TO_TYPE,true,true>{
        template<typename UNSIGNED_TYPE> 
            friend UNSIGNED_TYPE UnsignedBox::toUnsigned()const;

        static TO_TYPE convert(const FROM_TYPE v){
            if(v>static_cast<FROM_TYPE>(std::numeric_limits<TO_TYPE>::max())){
                std::ostringstream msg;
                msg<<v<<'>'<<
                    static_cast<FROM_TYPE>(std::numeric_limits<TO_TYPE>::max());
                throw std::logic_error(msg.str());
            }             
            return static_cast<TO_TYPE>(v);
        }
    };
}

template<typename UNSIGNED_TYPE> UNSIGNED_TYPE UnsignedBox::toUnsigned()const{
    return UnsignedBox_support::ToUnsigned<
        UnsignedBox::box_type,UNSIGNED_TYPE
    >::convert(this->value);    
    //TEMPLATE USE DEBUGGING:
    //If you find yourself here being told ToUnsigned has no member
    //convert() then it's possible you're trying to implement 
    //this member for a signed data-type.
}

int main(int argc, char*argv[]) {
    UnsignedBox box(
        static_cast<UnsignedBox::box_type>(std::numeric_limits<std::uint32_t>::max())+10
    );
    std::uint64_t v(box.toUnsigned<std::uint64_t>());
    std::cout<<v<<std::endl;

    try {
        std::uint32_t v(box.toUnsigned<std::uint32_t>());
    }catch(const std::logic_error err){
        std::cout<<err.what()<<std::endl;
    }
    return EXIT_SUCCESS;
}

关键在于实现一个成员函数(不能部分特化),并使其调用一个可以部分特化的类。

请注意,在支持类ToUnsigned中不声明成员convert(),滥用模板并尝试为有符号类型调用它会引发编译错误。否则,您可能面临更难以追踪的链接错误。 如果您在main()中添加了这样一行代码,我已经添加了一条注释,指向您可能需要查看的位置:

int xxx(box.toUnsigned<int>());

我必须说,我认为这不是一个丑陋的 hack,而是比任何 std::enable_if<> 解决方案更好,并且通过将支持成员设置为私有并使用 friend 来帮助实现它,至少可以一定程度上进行“封装”。

它还为进一步的特化留下了开放的大门,以补充或覆盖已经给出的内容。我并不是说所有模板都应该编写以允许进一步的特化,但我认为保持开放是有用的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接