Template specialization with std::enable_if<>

935 views Asked by At

The following code compiles and runs:

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

Expected output (all supporting platforms):

4294967305
4294967305>4294967295

So far so good.

But what I really want to do (for code clarity):

Is declare something like:

template<typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned()const;

Then provide specialized implementations such as:

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

But I get this error:

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;
                       ^

Which is odd because if you ask me the prototype of

UNSIGNED_TYPE UnsignedBox::toUnsigned() const

is a great match for

UNSIGNED_TYPE toUnsigned()const;

What am I doing wrong?

PS: This isn't the actual problem but my problem is an analogous where I want to special some templates based on the attributes of primitive types checked at compiled time.

2

There are 2 answers

5
Barry On BEST ANSWER

You can't declare a function with one signature:

template<typename UNSIGNED_TYPE>
UNSIGNED_TYPE toUnsigned() const;

and then define it with a different signature:

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;

The first one there takes one template argument, the second one takes two - even though one is defaulted. The two have to match completely. So you will need two declarations:

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;

And then the two definitions. Also this itself doesn't work because we're effectively redefining the default template argument, so you'd need to SFINAE on the return type, e.g.:

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;

Although it might be simpler to have the one toUnsigned() that forwards to two other member functions based on 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 */);
0
Persixty On

Following a bit of insight from Barry (above) I've determined what my answer should be:

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

The trick is to implement one member function (that can't partially specialised) and have it call out to a class (that can be partially specialised.

Notice by not declaring a member convert() in the support class ToUnsigned abusing the template and attempting to call it for a signed type provokes a compile error. Otherwise you risk a harder to trace link error. I've added a comment to the point you are likely to be taken to if you did add a line like this to main():

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

I have to say I think this is less an ugly hack than any std::enable_if<> solution and by making the support members private and friending the member they are there to help implement it does at least some work to 'encapsulate'.

It also leaves open the door to further specializations supplementing or overriding those already given. I'm not saying all templates should be written to permit further specialization but I do think it's useful to leave the door open.