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

4

5

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?

Cytogenesis answered 15/2, 2016 at 22:23 Comment(5)
@immibis: How does the preprocessor know that sizeof(size_t) == sizeof(unsigned long)?Northwester
A caveat: Even if sizeof(size_t) == sizeof(unsigned long), it's not guaranteed that they are the same type. size_t could also be a typedef to unsigned long long.Tangential
@immibis The preprocessor only does text processing. It knows nothing about the structure of the underlying text, and so can't predicate on C++ data type sizes. I suppose if the sizes of these types were defined as preprocessor macros in a standard way on all platforms, then you could use preprocessor conditionals to compare them, but I don't think such "standard macros" exist.Cytogenesis
Actually, I'm rethinking my previous comment... the byte count sizeof() values are not available to the preprocessor, but max value (and min value) macros like ULONG_MAX and SIZE_MAX should be available, so perhaps those would work... @immibis might have found a solution here.Cytogenesis
@Cytogenesis I've seen #if working with sizes of built-in types, but you're right in that size_t might not be a built-in type but a typedef.Metzler
T
5

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.

Thalweg answered 15/2, 2016 at 22:36 Comment(9)
If size_t and unsigned long are the same type, the third function template is ill-formed NDR.Socio
@T.C.: can you please elaborate? What is "NDR"? In my [tests}(coliru.stacked-crooked.com/a/b596b5b10651643e) it worked actually, but that could of coures be a compiler thing.Thalweg
"The program is ill-formed, no diagnostic required, if: no valid specialization can be generated for a template and that template is not instantiated".Socio
For those interested, here is a similar example plus explanation.Thalweg
@T.C.: I didn't get it yet, could you please explain more in detail what you mean? The specialization itself imo is well-formed (in the sense that it contains a valid program, unlike here). But the specialization is disabled through sfinae when size_t equals unsigned long. The only difference I see to usual sfinae is that the condition is never true. But why should sfinae make a difference between true, false and never true?Thalweg
The specialization includes both the declaration and the definition. There's no value of T such that std::enable_if_t<std::is_same<T, size_t>::value && !std::is_same<T, unsigned long>::value> is a valid type. The rules are sometimes annoying, but those are the rules :) A simple workaround is template<class T, class U> struct same : std::is_same<T, U> {}; and then use same instead of std::is_same. The difference is that same can be hypothetically specialized later...Socio
.. in a way such that same<X, Y>::value and !same<X, Y>::value are both true (overloaded !), but you aren't allowed to specialize std::is_same.Socio
Also, the standard forbids void* template non-type parameters, though all major implementations accept them :/ enable_if_t<..., int> = 0 is better.Socio
@T.C.: Thanks for the explanation. So, provided I write class no_sense{}; std::enable_if_t<(std::is_same<T, size_t>::value && !std::is_same<T, unsigned long>::value) || std::is_same<T, no_sense>::value>, the construct becomes valid? If so, it's annoying indeed.Thalweg
N
4

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

Niklaus answered 15/2, 2016 at 23:11 Comment(2)
For larger numbers of specializations, I'd go with this solutions (and use my answer only if it doesn't get more complicated as in the OP), so +1. I'd further wrap it in a namespace detail and use another function outside which calls the class member.Thalweg
Sometimes i solve the difficult part and oversee the obvious - of course, a global function calling the class-members even provides the easy syntax of a global function...Niklaus
H
2

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.

Heroine answered 15/2, 2016 at 23:45 Comment(0)
C
0

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

My solution is based on a comment made by @immibis which unfortunately has since been deleted. It was something like "Can't this be done easily using the preprocessor?" I realized that the C *_MAX macros from climits can in fact be used, and the solution is very simple. Thanks to @immibis!

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

Also, as @tbleher pointed out in his comment, sometimes same-size nominal types can be different true types, the example in question being unsigned long and unsigned long long. In fact, on my current system sizeof(unsigned long) == sizeof(unsigned long long) == 8, and ditto for the signed variants. Although they are the same size, they are considered different true types and will not conflict.

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

Here's 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

Output on my system:

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

So as it turned out, on my system, all conflictable types do in fact conflict with the true types unsigned long and long.

A couple of limitations of this solution are that it can only work for types that have corresponding *_MAX macros, and it doesn't work for floating-point types, since the preprocessor does not support floating-point arithmetic and comparisons.

Cytogenesis answered 19/2, 2016 at 2:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.