Search code examples
c++c++17c++20sfinaeoverload-resolution

How can I write positive/negative tests for C++ overload resolution and SFINAE?


I'm currently designing some functions that make use of SFINAE to control overload resolution. This is easy to get wrong, so I'd like to be able to write both positive and negative tests for what the API can be called with. For example:

// A templated function that should only be used via type deduction. (We
// don't want to let users to set template arguments explicitly in order
// to reserve the right to change them. They are not a public API.)
template <
    int&... ExplicitArgumentBarrier,
    typename T,
    typename = std::enable_if<!std::is_same_v<T, int>>>
void AcceptVector(std::vector<T>);

// We should be able to feed the function most vectors.
static_assert(kCanCallAcceptVector<std::vector<double>>);
static_assert(kCanCallAcceptVector<std::vector<std::string>>);

// But not vector<int>.
static_assert(!kCanCallAcceptVector<std::vector<int>>);

It feels like I should be able to do this with std::is_invocable_v, but that requires us to explicitly name the object being called, which requires providing template arguments and which assumes something about the overload set. Instead I literally want to test overload resolution: regardless of how many overloads for AcceptVector there are and what their template arguments look like, is a call site spelled AcceptVector that provides a particular type as argument valid?

What's the best way to do this? Here is what I've come up with:

#define DEFINE_CAN_CALL_VARIABLE(namespace_name, function_name)           \
  template <typename... Args>                                             \
  struct internal_CanCall##function_name final {                          \
   private:                                                               \
    struct Functor {                                                      \
      template <typename... U,                                            \
                typename = decltype(::namespace_name ::function_name(     \
                    std::declval<U>()...))>                               \
      void operator()(U&&... args);                                       \
    };                                                                    \
                                                                          \
   public:                                                                \
    static constexpr bool kValue = std::is_invocable_v<Functor, Args...>; \
  };                                                                      \
                                                                          \
  template <typename... Args>                                             \
  inline constexpr bool kCanCall##function_name =                         \
      internal_CanCall##function_name<Args...>::kValue;

DEFINE_CAN_CALL_VARIABLE(my::project, AcceptVector);

The idea is to define a functor that we can name for std::is_invocable_v, and then lean on overload resolution in that functor. But there are some downsides:

  • I have to package it into a macro in order to make it reusable without having the same problem as std::is_invocable_v over again.

  • I need to have the user provide the namespace as a macro argument in order to avoid problems due to argument-dependent lookup. (Although I said I'm interested in any call spelled AcceptVector, I guess I really mean any call spelled AcceptVector that resolves to an AcceptVector function in my project's namespace.)

  • I'm not 100% sure I haven't missed other edge cases.

Does it seem like this is correct? Is there a less awful way to do this?


Solution

  • Since you tagged this C++20, the better approach is to write a concept:

    template <typename... Args>
    concept canAcceptVector = requires (Args(*args)()...) {
        AcceptVector(args()...);
    };
    

    Which you can then test:

    // We should be able to feed the function most vectors.
    static_assert(canAcceptVector<std::vector<double>>);
    static_assert(canAcceptVector<std::vector<std::string>>);
    
    // But not vector<int>.
    static_assert(!canAcceptVector<std::vector<int>>);
    

    The odd formulation with the function pointer is to ensure that canAcceptVector<int>, canAcceptVector<int&>, and canAcceptVector<int&&> try to call AcceptVector with a prvalue, lvalue, and xvalue respectively. Probably not super important in this case, but I find it slightly less unwieldy than dealing with std::forward and plus it correctly deals with prvalues.


    The best C++17 approach for this problem is the detection idiom, which requires an extra step over concepts:

    template <typename... Args>
    using AcceptVector_t = decltype(AcceptVector(std::declval<Args>()...));
    
    template <typename... Args>
    inline constexpr bool canAcceptVector = is_detected_v<AcceptVector_t, Args...>;
    

    Which you can static_assert in the same way as above.