Search code examples
c++c++17template-meta-programmingsfinae

Function template detection through SFINAE


I want to write a function that dispatches a lambda either to an external library function template (if this function template exists within the library) or to execute the lambda directly. We can do this using SFINAE shown below:

#include <iostream>
#include <type_traits>

#ifdef _HAS_THE_FUNC
//This part comes from an external library that either provides the function (in an older version) or not (in a newer version).
namespace ext { namespace lib {
  template<int N = 1, typename F>
  void lib_fun(F f) {
    std::cout << "LibFun<" << N << ">" << std::endl;
    f();
  }
} }
#endif
// "Inject" a little helper into the library's namespace to detect the presence of the function
namespace ext { namespace lib {
  // Helper type to detect the presence of lib_fun
  template <typename F>
  struct has_lib_fun {
    template <typename Fun>
    static auto test(int) -> decltype(lib_fun<int{}>(std::declval<Fun>()), std::true_type{});

    template <typename>
    static auto test(...) -> std::false_type;

    using type = decltype(test<F>(0));
    static constexpr bool value = type::value;
  };
  // Use SFINAE to conditionally select the return type of loop_fuse
  template <int N = 1, typename F>
  auto safe_lib_fun(F f, int) -> std::enable_if_t<has_lib_fun<F>::value> {
    lib_fun<N>(f);
  }
  template <int N = 1, typename F>
  auto safe_lib_fun(F f, double) {
    std::cout << "Workaround" << std::endl;
    return f();
  }
} }

int main() {
  ext::lib::safe_lib_fun([]() { std::cout << "Hello World" << std::endl; }, 0);
  ext::lib::safe_lib_fun<2>([]() { std::cout << "Hello World" << std::endl; }, 0);
  return 0;
}

If I build and run the code, I get the expected output: clang++ -std=c++17 main.cpp -o woFun && clang++ -std=c++17 main.cpp -D_HAS_THE_FUNC -o wFun

#./woFun
Workaround
Hello World
#./wFun
LibFun<1>
Hello World

However, when I try to build the code with GCC using c++17 (everything is fine when using c++20), I get the following errors (if the function template is not present):

<source>:20:39: error: 'lib_fun' was not declared in this scope; did you mean 'has_lib_fun'?
   20 |     static auto test(int) -> decltype(lib_fun<int{}>(std::declval<F>()), std::true_type{});
      |                                       ^~~~~~~
      |                                       has_lib_fun
<source>:20:39: error: 'lib_fun' was not declared in this scope; did you mean 'has_lib_fun'?
   20 |     static auto test(int) -> decltype(lib_fun<int{}>(std::declval<F>()), std::true_type{});
      |                                       ^~~~~~~
      |                                       has_lib_fun
<source>: In function 'std::enable_if_t<ext::lib::has_lib_fun<_F>::value> ext::lib::safe_lib_fun(_F, int)':
<source>:31:5: error: 'lib_fun' was not declared in this scope; did you mean 'has_lib_fun'?
   31 |     lib_fun<_N>(f);
      |     ^~~~~~~

How can I check for the existence of the function template prior with C++11/17?


Solution

  • What you're trying to do is impossible without some changes to lib_fun.

    template<int N = 1, typename F>
    void lib_fun(F f) {
      std::cout << "LibFun<" << N << ">" << std::endl;
      f();
    }
    

    The problem here is that N is not deducible from the function parameters, so you would have to call it like lib_fun<N>(f) in your expression tester (unless you never provided N, but that defeats the purpose of this template parameter). This runs into the problem:

    Knowing which names are type names allows the syntax of every template to be checked. The program is ill-formed, no diagnostic required, if:

    • [...]
    • a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, or

    - [temp.res] p8.3

    The syntax lib_fun<N>(f) in your expression tester has_lib_fun is always ill-formed if lib_fun isn't defined at that point. The reason why lib_fun(f) would work is that hypothetically, lib_fun could be found through argument dependent lookup (ADL) for some arguments f, so the compiler cannot reject it with an error despite lib_fun not being defined.

    Solution

    #ifdef _HAS_THE_FUNC
    namespace ext { namespace lib {
      template<int _N = 1, typename _F>
      void lib_fun(_F f) {
        std::cout << "LibFun<" << _N << ">" << std::endl;
        f();
      }
    } }
    #else
    namespace ext { namespace lib {
      template<int _N = 1, typename _F>
      void lib_fun(_F f) = delete;
    } }
    #endif
    

    See live solution at Compiler Explorer.

    If lib_fun is deleted, not undefined when _HAS_THE_FUNC is undefined, then the expression lib_fun<N>(f) could still be valid, hypothetically.

    It wouldn't be valid once instantiated because lib_fun is defined as deleted, but it hypothetically could be valid for some instantiations of has_lib_fun and specializations of lib_fun, so the compiler can't just reject it with an error.

    Alternative Solution

    You've mentioned in the comments that in practice, there is no _HAS_THE_FUNC, so you cannot define a replacement. However, if you can't define a replacement, you can simply define an overload:

    struct incomplete;
    
    template<int _N = 1, typename _F> // we can't just write enable_if_t<false>
    std::enable_if_t<std::is_same_v<_F, incomplete>> lib_fun(_F f);
    

    See live solution at Compiler Explorer.

    If incomplete was defined at some point, then hypothetically, _F could be the same as incomplete. This is why the compiler can't just reject this. However, we never define incomplete in practice, and thus lib_fun is a function for which SFINAE always fails.

    Further Notes

    All names starting with an underscore followed by a capital letter are reserved for the implementation. You should avoid names like _HAS_THE_FUNC.

    As a side note, namespace ext { namespace lib can be simplified to namespace ext::lib since C++17.