Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to conditionally disable a global function definition using template metaprogramming?

Say I have a simple nullary template function templated on a single parameter, with two specializations, one for unsigned long, and one for size_t (contents not important):

template<typename T> T f(void);
template<> unsigned long f<unsigned long>(void) { return 1; }
template<> size_t f<size_t>(void) { return 2; }

My understanding is that the exact definition of the type size_t is platform-dependent, and so it may or may not be equivalent to unsigned long. On my current platform (Cygwin g++ 5.2.0 on Windows 10 64-bit compiling with -std=gnu++1y) these two types appear to be equivalent, so the above code fails to compile:

../test.cpp:51:19: error: redefinition of ‘T f() [with T = long unsigned int]’
 template<> size_t f<size_t>(void) { return 2; }
                   ^
../test.cpp:50:26: note: ‘T f() [with T = long unsigned int]’ previously declared here
 template<> unsigned long f<unsigned long>(void) { return 1; }
                          ^

From my thinking, this problem could be solved by simply disabling the size_t function definition, since any code that attempted to call f<size_t>() would automatically resolve to f<unsigned long>(). But the function should be enabled for platforms that define size_t to be different from unsigned long.

I've read a little bit about template metaprogramming and SFINAE, and I've been playing with things like this:

std::enable_if<(sizeof(size_t) > sizeof(unsigned long))>::type 

But I'm unsure how to use such a fragment to disable a global function definition, if that's possible at all.

So, is there any way to conditionally disable a global function definition using template metaprogramming? Or, more generally, am I on the right track, or heading down a wrong path?

like image 589
bgoldst Avatar asked Feb 15 '16 22:02

bgoldst


3 Answers

This works in any case, but it's a bit tedious and doesn't scale that well for a larger number of specializations:

template<typename T
       , std::enable_if_t<!std::is_same<T, unsigned long>::value
                       && !std::is_same<T, size_t>::value>* = nullptr>
T f() { return 1; }

template<typename T
       , std::enable_if_t<std::is_same<T, unsigned long>::value>* = nullptr>
T f() { return 2; }

template<typename T
       , std::enable_if_t<std::is_same<T, size_t>::value
                      && !std::is_same<T, unsigned long>::value>* = nullptr>
T f() { return 3; }

The idea: don't specialize but overload, and enable the overloads only if the signature is appropriate (while disabling the others at the same time).

Further, in order to make it better maintainable, one should outsource the logical checks into another suitable class.


DEMO:

int main()
{
    std::cout<<f<int>()<<std::endl;
    std::cout<<f<unsigned long>()<<std::endl;
    std::cout<<f<size_t>()<<std::endl;
    std::cout<<f<unsigned long long>()<<std::endl;
}

It prints:

1
2
2
1

So it seems "size_t == unsigned long" on coliru.

like image 54
davidhigh Avatar answered Sep 22 '22 20:09

davidhigh


From my experience: Not with global functions directly (reading davidhigh's answer while typing: ok, it works, but as he said it doesnt scale well). SFINAE only works, if the "error" appears while resolving template parameters. Since C++ allows function templates to be fully specialized only, there is no "resolving" when the compiler tries to compile a specialisation.

However with classes the compiler allows for partial specializations and you can do something like this, which has the benefit that you need the SFINAE-expression only for the size_t (using a mySize here since i can change it):

#include <iostream>
#include <type_traits>
using namespace std;

typedef unsigned int mySize;

//default
template <class P, class dummy = void>
class T{
    public:
   static P f(){return 0;}
};

//int
template <>
class T<int,void> {
    public:
   static int f(){return 1;}
};

//unsigned long
template <>
class T<unsigned long, void> {
    public:
   static unsigned long f(){return 2;}
};

template <class P>
class T<P, typename enable_if<is_same<P, mySize>::value && !is_same<P, unsigned long>::value, void>::type> {
    public:
   static P f(){return 3;}
};

int main() {
    cout << T<int>::f() << endl;
    cout << T<unsigned long>::f() << endl;
    cout << T<mySize>::f() << endl;
    return 0;
}

Output with typedef unsigned long mySize;:

1
2
2

Output with any other typedef (well, not int for obvious reasons):

1
2
3

Try it online

like image 36
Anedar Avatar answered Sep 24 '22 20:09

Anedar


Here's an approach that is a bit strange, but fairly easy to work with:

//using MyType = unsigned int;
using MyType = unsigned long;

unsigned long f2(MyType *,int) { return 1; }
size_t        f2(size_t *,...) { return 2; }

template <typename T>
auto f() -> decltype(f2(static_cast<T*>(0),0)) {
    T* p = 0;
    return f2(p,0);
}

int main()
{
    std::cout << f<MyType>() << "\n";
    std::cout << f<size_t>() << "\n";
}

The idea here is that you can make a distinct function for the size_t case which will not be preferred, but will be used if there is no other option. If size_t and MyType are the same, then the MyType overload will be used, otherwise the size_t overload will be used.

f calls f2 and uses a trailing return type with decltype so that if f2 doesn't exist for a particular type T, then f won't exist either.

Using this technique, you can easily add overloads for other types as well.

like image 43
Vaughn Cato Avatar answered Sep 24 '22 20:09

Vaughn Cato