Is it possible to conditionally disable the definition of a global function using metaprogramming templates?

Let's say I have a simple template function with a zero template built on one parameter, with two specializations: one for unsigned long and one for size_t (content is 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; } 

I understand that the exact definition of type size_t is platform dependent, and therefore it may or may not be equivalent to an unsigned long . On my current platform (Cygwin g ++ 5.2.0 on a 64-bit compilation of Windows 10 with -std=gnu++1y ) these two types look equivalent, so the code above does not 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; } ^ 

In my opinion, this problem can be solved by simply disabling the definition of the size_t function, since any code that tried to call f<size_t>() would automatically resolve to f<unsigned long>() . But the function must be enabled for platforms that define size_t as different from unsigned long .

I read a little about metaprogramming templates and SFINAE, and I played with things like this:

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

But I'm not sure how to use such a fragment to disable the definition of a global function, if at all possible.

So, is there a way to conditionally disable the definition of a global function using metaprogramming templates? Or, in a more general sense, am I on the right path or am I missing the wrong path?

+5
source share
4 answers

This works anyway, but it is a bit tedious and does not scale for more 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: not to specialize, but to overload and activate overloads only if the signature is appropriate (while disabling others).

In addition, to make it better supported, you should pass logical checks to 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; } 

He prints:

 1 2 2 1 

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

+5
source

From my experience: not with global functions directly (reading a davidhigh answer during input: ok, it works, but as he said, it doesn't scale well). SFINAE only works when an β€œerror” occurs when resolving template parameters. Since C ++ only allows function templates to fully specialize, there is no β€œpermission” when the compiler tries to compile specialization.

However, with classes, the compiler allows partial specialization, and you can do something like this, which has the advantage that you only need the SFINAE expression for size_t (using 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; } 

Exit 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

+4
source

Here's an approach that is a bit strange, but pretty simple 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 separate function for the case size_t, which will not be preferred, but will be used if there is no other option. If size_t and MyType same, then MyType overload will be used, otherwise size_t overload will be used.

f calls f2 and uses the return return type with decltype , so if f2 does not exist for a specific type T, then f will not exist either.

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

+2
source

I accepted @ davidhigh's answer because I think this is the most suitable solution for my question, as was asked, however, I used a different solution in my actual code, and just in case it helps others, I will describe it here.

My decision is based on comments made by @immibis, which unfortunately has since been removed. It was something like, "Could this be done easily using a preprocessor?" I realized that the C *_MAX from climits can actually be used, and the solution is very simple. Thanks @immibis!

I applied preprocessor protection to all types that could cause a conflict, both for signed and unsigned options. This consisted of size_t , uintmax_t , ssize_t , ptrdiff_t and intmax_t .

In addition, as @tbleher noted in his comment, sometimes nominal types of the same size can be different true types, for example, unsigned long and unsigned long long . In fact, in my current system, sizeof(unsigned long) == sizeof(unsigned long long) == 8 , as well as for signed options. Although they are the same size, they are considered different true types and do not conflict.

My approach was to first define a function for each of the guaranteed distinct types, then define a conceptual order for the "conflicting" types, and then gradually create a definition for each conflicting type whose size is (1) greater than the size [unsigned] long long and (2), not equal to the size of any conflicting type that sits earlier in the order.

Here is a demo:

 #include <climits> // most integer limit macros, including SSIZE_MAX #include <cstddef> // size_t, ptrdiff_t, [u]intmax_t #include <cstdint> // SIZE_MAX, PTRDIFF_{MIN,MAX}, UINTMAX_MAX, INTMAX_{MIN,MAX} #include <sys/types.h> // ssize_t #include <cstdio> // primary template template<typename T> void f(void); // declarations -- guaranteed not to cause conflicts; dups are allowed template<> void f<unsigned char>(void); template<> void f<unsigned short>(void); template<> void f<unsigned int>(void); template<> void f<unsigned long>(void); template<> void f<unsigned long long>(void); template<> void f<size_t>(void); template<> void f<uintmax_t>(void); template<> void f<char>(void); template<> void f<short>(void); template<> void f<int>(void); template<> void f<long>(void); template<> void f<long long>(void); template<> void f<ssize_t>(void); template<> void f<ptrdiff_t>(void); template<> void f<intmax_t>(void); int main(void) { f<unsigned char>(); f<unsigned short>(); f<unsigned int>(); f<unsigned long>(); f<unsigned long long>(); f<size_t>(); f<uintmax_t>(); f<char>(); f<short>(); f<int>(); f<long>(); f<long long>(); f<ssize_t>(); f<ptrdiff_t>(); f<intmax_t>(); return 0; } // end main() // definitions -- must use preprocessor guard on conflictable types template<> void f<unsigned char>(void) { std::printf("%d\n",1); } template<> void f<unsigned short>(void) { std::printf("%d\n",2); } template<> void f<unsigned int>(void) { std::printf("%d\n",3); } template<> void f<unsigned long>(void) { std::printf("%d\n",4); } template<> void f<unsigned long long>(void) { std::printf("%d\n",5); } #if SIZE_MAX > ULLONG_MAX template<> void f<size_t>(void) { std::printf("%d\n",6); } #endif #if UINTMAX_MAX > ULLONG_MAX && UINTMAX_MAX != SIZE_MAX template<> void f<uintmax_t>(void) { std::printf("%d\n",7); } #endif template<> void f<char>(void) { std::printf("%d\n",8); } template<> void f<short>(void) { std::printf("%d\n",9); } template<> void f<int>(void) { std::printf("%d\n",10); } template<> void f<long>(void) { std::printf("%d\n",11); } template<> void f<long long>(void) { std::printf("%d\n",12); } #if SSIZE_MAX > LLONG_MAX template<> void f<ssize_t>(void) { std::printf("%d\n",13); } #endif #if PTRDIFF_MAX > LLONG_MAX && PTRDIFF_MAX != SSIZE_MAX template<> void f<ptrdiff_t>(void) { std::printf("%d\n",14); } #endif #if INTMAX_MAX > LLONG_MAX && INTMAX_MAX != SSIZE_MAX && INTMAX_MAX != PTRDIFF_MAX template<> void f<intmax_t>(void) { std::printf("%d\n",15); } #endif 

The output on my system is:

 1 2 3 4 5 4 4 8 9 10 11 12 11 11 11 

So, as it turned out, in my system all conflicting types really conflict with the true types of unsigned long and long .

A couple of limitations of this solution is that it can only work for types with the corresponding *_MAX macros, and it does not work for floating point types, because the preprocessor does not support floating point arithmetic and comparison.

0
source

Source: https://habr.com/ru/post/1243093/


All Articles