How to check if a function template has been specialized?
Asked Answered
U

2

7

Is there a way to establish at compile time if a certain function template was specialized?

For example, assume the following function template:

template<size_t N>
void foo();

I want to test if foo<42> was specialized. Note that the declaration above doesn't contain any default implementation.

I tried SFINAE but couldn't find a condition on the function that the compiler cannot deduce from its declaration.

Unreason answered 28/3, 2017 at 14:46 Comment(6)
Hmm, where and why would you use something like this? Perhaps a more elegant solution to your initial problem is out there.Haul
Well, I can't use overloads and SFINAE because I have a non-type template parameter. I can however add an unnamed std::integral_constant<size_t, 42> to the functions. But I was hoping there might be a cleaner solution out there...Unreason
non-type template parameters don't prevent you from using overloads or SFINAE. It's still not clear why you would want to detect this.Enchiridion
It is not clear what the actual problem here is.Murrah
Let's say I'm developing a header-based library that allows the user to specialize foo as many times as he likes, then call a template function in the library with a certain size_t template argument, and this function will delegate to the function that was specialized with the nearest number. Is that possible?Unreason
I'm removing the [c++11] tag from this question since it unnecessarily constrains solutions to that revision, which doesn't seem relevant to the question asked.Mariannamarianne
A
4

Is there a way to establish in compile time if a certain template function was specialized?

With a function... I don't think so.

But if you create a functor, you can add a static const member (is_specialized, in the following example) that can give you this information

#include <iostream>

template <std::size_t N>
struct foo
 {
   static constexpr bool is_specialized { false };

   void operator() () const
    { std::cout << "- generic (" << N << ") foo struct" << std::endl; }
 };

template <>
struct foo<42U>
 {
   static constexpr bool is_specialized { true };

   void operator() () const
    { std::cout << "- specialized (42) foo struct" << std::endl; }
 };

int main()
 {
   foo<17U>()(); // print - generic (17) foo struct
   foo<42U>()(); // print - specialized (42) foo struct

   std::cout << foo<17U>::is_specialized << std::endl; // print 0
   std::cout << foo<42U>::is_specialized << std::endl; // print 1
 }

--- EDIT ---

Following the suggestion from Quentin (thanks again!) I've developed another functor-based solution that use something, to detect if the functor is generic or specialize, that is added only in the generic functor. In this case, a type instead a bool constant.

template <std::size_t N>
struct foo
 {
   // im_not_specialized is added only in the generic version!
   using im_not_specialized = void;

   void operator () () const
    { std::cout << "- generic (" << N << ") foo struct" << std::endl; }
 };

template <>
struct foo<42U>
 {
   void operator () () const
    { std::cout << "- specialized (42) foo struct" << std::endl; }
 };

This type can be used via SFINAE and I propose an example based on a constexpr isSpecialized() template function (with an helper function)

template <typename F>
constexpr bool isSpecializedHelper
      (int, typename F::im_not_specialized const * = nullptr)
 { return false; }

template <typename F>
constexpr bool isSpecializedHelper (long)
 { return true; }

template <typename F>
constexpr bool isSpecialized ()
 { return isSpecializedHelper<F>(0); }

This require a little more work but isSpecialized() can be reused with different functors (im_not_specialized type based)

The following is a full working example

#include <iostream>

template <std::size_t N>
struct foo
 {
   // im_not_specialized is added only in the generic version!
   using im_not_specialized = void;

   void operator () () const
    { std::cout << "- generic (" << N << ") foo struct" << std::endl; }
 };

template <>
struct foo<42U>
 {
   void operator () () const
    { std::cout << "- specialized (42) foo struct" << std::endl; }
 };

template <typename F>
constexpr bool isSpecializedHelper
      (int, typename F::im_not_specialized const * = nullptr)
 { return false; }

template <typename F>
constexpr bool isSpecializedHelper (long)
 { return true; }

template <typename F>
constexpr bool isSpecialized ()
 { return isSpecializedHelper<F>(0); }

int main()
 {
   foo<17U>()(); // print - generic (17) foo struct
   foo<42U>()(); // print - specialized (42) foo struct

   constexpr auto isSp17 = isSpecialized<foo<17U>>();
   constexpr auto isSp42 = isSpecialized<foo<42U>>();

   std::cout << isSp17 << std::endl; // print 0
   std::cout << isSp42 << std::endl; // print 1
 }
Ardisardisj answered 28/3, 2017 at 15:16 Comment(9)
One step further: declare a dummy using im_not_specialized = void; in the base template, and detect it later. Now the user does not have to do anything extra :)Crumpler
@Crumpler - not sure to undestand... im_not_specialized should be added to is_specialized or used to substitute it? And when should be used im_not_specialized?Ardisardisj
It replaces is_specialized. Then you can use SFINAE to detect whether the foo<?> you're looking at contains the typedef (then it's from the base template) or not (then it has been specialized).Crumpler
@Crumpler - I see... interesting solution; you should propose it as an answerArdisardisj
Well, it still hinges on using functors. I'm searching for a solution that keeps the function templates.Crumpler
@Crumpler - I look forward to see your template based solution; I'm not very good in SFINAE but, if you don't worry, I try to add, in my answer, an example based on your im_not_specializedArdisardisj
@Crumpler Interesting. But if i'd be using functors that I could easily test for sizeof<Foo<N>> in SFINAE. This will fail for an N that hasn't been introduced with a specialization.Unreason
@Ardisardisj Go ahead :)Crumpler
There is always the possibility a user might write template<>class foo<42>:foo<41>{...}. To be more robust, you need using default_for_type = foo; so you can check if this is the default for the right type.Deepfry
A
4

If you mark the base function as deleted (= delete), you can detect if it has been specialized using SFINAE (assuming the specialization itself is not deleted)

An expression like decltype(foo<N>()) will result in a substitution failure if foo<N> is marked as deleted. If you provide a specialization that is not deleted on the other hand the expression will not result in an error.

Using this you can create a simple trait class to check if foo has been specialized for a specific set of template parameters:

template<std::size_t N, class = void>
struct is_foo_specialized : std::false_type {};

template<std::size_t N>
struct is_foo_specialized<N, decltype(foo<N>(), void())> : std::true_type {};

1. Basic examples

C++11: godbolt

#include <type_traits>

template<std::size_t N>
void foo() = delete;

template<>
void foo<1>() { }

template<std::size_t N, class = void>
struct is_foo_specialized : std::false_type {};

template<std::size_t N>
struct is_foo_specialized<N, decltype(foo<N>(), void())> : std::true_type {};

int main()
{
    static_assert(!is_foo_specialized<0>::value, ""); // foo<0> is not specialized
    static_assert(is_foo_specialized<1>::value, ""); // foo<1> IS specialized
}

With C++20 you could also use a concept for this, e.g.: godbolt

#include <type_traits>

template<std::size_t N>
void foo() = delete;

template<>
void foo<1>() { }

template<std::size_t N>
concept is_foo_specialized = requires { foo<N>(); };

int main()
{
    static_assert(!is_foo_specialized<0>); // foo<0> is not specialized
    static_assert(is_foo_specialized<1>); // foo<1> IS specialized
}

2. Providing a default implementation

Due to the function being = delete'd it can't have a default implementation.

If you do require a default implementation for the function, you could use 2 functions instead:

  • one that is = delete'd (so SFINAE can detect it)
  • and another one that implements the default behaviour and forwards to the other if a specialization exists

C++11: godbolt

#include <type_traits>
#include <iostream>

template<std::size_t N>
void foo_specialized() = delete;

template<>
void foo_specialized<1>() { std::cout << "CUSTOMIZED!" << std::endl; }

template<std::size_t N, class = void>
struct is_foo_specialized : std::false_type {};

template<std::size_t N>
struct is_foo_specialized<N, decltype(foo_specialized<N>(), void())> : std::true_type {};

template<std::size_t N>
typename std::enable_if<!is_foo_specialized<N>::value>::type foo() {
    std::cout << "DEFAULT!" << std::endl;
}

template<std::size_t N>
typename std::enable_if<is_foo_specialized<N>::value>::type foo() {
    foo_specialized<N>();
}

int main()
{
    foo<0>(); // -> DEFAULT!
    foo<1>(); // -> CUSTOMIZED!
}

Or with C++20: godbolt

#include <type_traits>
#include <iostream>

template<std::size_t N>
void foo_specialize() = delete;

template<>
void foo_specialize<1>() { std::cout << "CUSTOMIZED!" << std::endl; }

template<std::size_t N>
concept is_foo_specialized = requires { foo_specialize<N>(); };


template<std::size_t N> requires (!is_foo_specialized<N>)
void foo() {
    std::cout << "DEFAULT!" << std::endl;
}

template<std::size_t N> requires (is_foo_specialized<N>)
void foo() {
    foo_specialize<N>();
}

int main()
{
    foo<0>(); // -> DEFAULT!
    foo<1>(); // -> CUSTOMIZED!
}

3. Compile-time shenanigans

This can of course also be used to iterate the specializations (within a certain limit) - or like you asked in the comments to find the nearest specialization of the function.

nearest_foo_specialized in this example will iterate over a range of values for N and check if a specialization of foo exists for this value.

  • N is the value where we want to start the search
  • SearchRange determines how many specializations will be checked (both up and down) from the provided N value (in this example we check for N's +/- 10)
  • CurrentDistance keeps track how far we've already searched from our starting N value, so we don't exceed the specified SearchRange
  • The last template parameter is used for SFINAE

e.g.:

nearest_foo_specialized<100, 10> would check for specializations of foo between N = 90 and N = 110, returning the one that is closer to 100 (prefering lower N values in case of a draw)

Example C++11: godbolt

#include <type_traits>
#include <iostream>
#include <utility>

template<std::size_t N>
void foo() = delete;

template<>
void foo<5>() { std::cout << 5 << std::endl; }

template<>
void foo<10>() { std::cout << 10 << std::endl; }

template<>
void foo<15>() { std::cout << 15 << std::endl; }

template<std::size_t N, class = void>
struct is_foo_specialized : std::false_type {};

template<std::size_t N>
struct is_foo_specialized<N, decltype(foo<N>(), void())> : std::true_type {};

template<std::size_t N, std::size_t SearchRange = 10, std::size_t CurrentDistance = 0, class = void>
struct nearest_foo_specialized {
    static const std::size_t index = 0; // an index for which foo<> is specialized, if value is true.
    static const std::size_t distance = CurrentDistance; // distance from original N
    static const bool value = false; // have we found a specialization yet?
};

// Found a match -> Report Success
template<std::size_t N, std::size_t SearchRange, std::size_t CurrentDistance>
struct nearest_foo_specialized<N, SearchRange, CurrentDistance, typename std::enable_if< CurrentDistance <= SearchRange && is_foo_specialized<N>::value >::type> {
    static const std::size_t index = N;
    static const std::size_t distance = CurrentDistance;
    static const bool value = true;
};

// No match found -> recurse until SearchRange limit
template<std::size_t N, std::size_t SearchRange, std::size_t CurrentDistance>
struct nearest_foo_specialized<N, SearchRange, CurrentDistance, typename std::enable_if< CurrentDistance < SearchRange && !is_foo_specialized<N>::value >::type> {
    typedef nearest_foo_specialized<N - 1, SearchRange, CurrentDistance + 1> down;
    typedef nearest_foo_specialized<N + 1, SearchRange, CurrentDistance + 1> up;

    static const std::size_t distance = down::distance < up::distance ? down::distance : up::distance;
    static const std::size_t index = down::distance == distance && down::value ? down::index : up::index;
    static const std::size_t value = down::distance == distance && down::value ? down::value : up::value;
};

// calls the nearest foo() specialization (up to 10 away from the specified N)
template<std::size_t N>
typename std::enable_if<nearest_foo_specialized<N>::value>::type call_nearest_foo() {
    foo<nearest_foo_specialized<N>::index>();
}

template<std::size_t N>
typename std::enable_if<!nearest_foo_specialized<N>::value>::type call_nearest_foo() {
    static_assert(N!=N, "No nearest foo() specialization found!");
}


int main() {
    call_nearest_foo<7>(); // calls foo<5>()
    call_nearest_foo<8>(); // calls foo<10>()

    call_nearest_foo<11>(); // calls foo<10>()
    call_nearest_foo<15>(); // calls foo<15>()

    call_nearest_foo<25>(); // calls foo<15>()
    // call_nearest_foo<26>(); // error: No nearest foo() (only searching up to 10 up / down)
}
Anesthetist answered 23/5, 2022 at 19:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.