如何为 C++ 重载解析和 SFINAE 编写 positive/negative 测试?

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

我目前正在设计一些使用 SFINAE 来控制重载解析的函数。这很容易出错,所以我希望能够针对可以调用 API 的内容编写正面和负面测试。例如:

// 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>>);

感觉我应该可以用 std::is_invocable_v 做到这一点,但这需要我们显式命名被调用的对象,这需要提供模板参数并假定一些关于重载集的信息。相反,我真的想测试重载解析:无论 AcceptVector 有多少重载以及它们的模板参数是什么样的,拼写为 AcceptVector 的调用站点是否提供特定类型作为有效参数?

最好的方法是什么?这是我想出的:

#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);

我们的想法是定义一个我们 可以 std::is_invocable_v 命名的仿函数,然后依靠该仿函数中的重载解析。但也有一些缺点:

这样看起来对吗?有没有更简单的方法来做到这一点?

既然你标记了这个 C++20,更好的方法是写一个概念:

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

然后你可以测试:

// 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>>);

函数指针的奇怪表述是为了确保 canAcceptVector<int>canAcceptVector<int&>canAcceptVector<int&&> 尝试使用纯右值、左值和 xvalue 调用 AcceptVector分别。在这种情况下可能不是特别重要,但我发现它比处理 std::forward 稍微不那么笨拙,而且它正确地处理 prvalues.


解决此问题的最佳 C++17 方法是 detection idiom,它需要对概念进行额外的步骤:

template <typename... Args>
using AcceptVector_t = decltype(AcceptVector(std::declval<Args>()...));

template <typename... Args>
inline constexpr bool canAcceptVector = is_detected_v<AcceptVector_t, Args...>;

可以static_assert和上面一样