出了一道让学生造个简单的 Polynomial
的题,主要是练习 class
以及 operator overloading。为了快速地检验学生的实现是否符合我们对各个成员函数的要求,C++20 concepts 可以发挥神力:
namespace detail {
template <typename T, typename... U>
concept any_of = (std::same_as<T, U> || ...);
} // namespace detail
template <typename T>
concept correct_polynomial
= requires(T p, const T cp, std::size_t i, double x) {
{ p[i] } -> std::same_as<double &>;
{ cp[i] } -> detail::any_of<double, const double, const double &>;
{ cp(x) } -> detail::any_of<double, const double>;
{ -cp } -> detail::any_of<T, const T>;
{ cp + cp } -> detail::any_of<T, const T>;
{ cp - cp } -> detail::any_of<T, const T>;
{ cp * cp } -> detail::any_of<T, const T>;
{ p += cp } -> std::same_as<T &>;
{ p -= cp } -> std::same_as<T &>;
{ p *= cp } -> std::same_as<T &>;
{ cp.derivative() } -> detail::any_of<T, const T>;
{ cp.integral() } -> detail::any_of<T, const T>;
{ cp == cp } -> std::same_as<bool>;
{ cp != cp } -> std::same_as<bool>;
};
static_assert(correct_polynomial<Polynomial>);
(此处省去了一些对构造、拷贝控制等成员的要求)
这种写法不仅简单清楚,而且能产生非常好的报错信息:
compile_test.cpp:65:15: error: static assertion failed
65 | static_assert(correct_polynomial<Polynomial>);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compile_test.cpp:65:15: note: constraints not satisfied
compile_test.cpp:41:9: required by the constraints of ‘template<class T> concept correct_polynomial’
compile_test.cpp:47:10: in requirements with ‘T p’, ‘const T cp’, ‘std::size_t i’, ‘double x’ [with T = Polynomial]
compile_test.cpp:57:11: note: ‘p -= cp’ does not satisfy return-type-requirement
57 | { p -= cp } -> std::same_as<T &>;
它可以明确地告诉我 { p -= cp } -> std::same_as<T &>
这一条没有满足,而不是一些 no match for call to function xxxx 之类的错误(虽然后者在报错点的上下文比较清晰的情况下也还算可读)。
遗憾的是我们课程是 based on C++17 的,这是出于多方面的考虑,在这里不多解释。想要在 C++17 里做到这个效果(即捕捉未满足的 requirements,并自定义报错信息)是可能的,但就需要 SFINAE:
namespace detail {
template <typename P = Polynomial, typename CP = const Polynomial,
typename R = decltype(std::declval<P &>() -= std::declval<CP &>()),
typename = std::enable_if_t<std::is_same_v<R, P &>>>
std::true_type helper(int);
std::false_type helper(...);
} // namespace detail
static_assert(decltype(detail::helper(0))::value,
"Expect { p -= cp } -> Polynomial &");
这样写可以达到效果,但是也太费事了一点,关键是编译器在遇到 substitution failure 的时候并不告诉我具体是哪一个 substitution fail 了,而是悄无声息地转向另一个 overload candidate,于是我们也不能将所有 requirements 都写在一个 helper 里。此外,到处带着 std::declval<...>()
实在是麻烦。
仔细考虑之后我捣鼓出这么个玩意:
template <typename T, typename... Types>
using enable_if_any = std::enable_if_t<(std::is_same_v<T, Types> || ...)>;
#define EXPECT(NAME, EXPR, ...) \
template <typename P = Polynomial, typename CP = const Polynomial, \
typename R = decltype(EXPR), \
typename = enable_if_any<R, __VA_ARGS__>> \
std::true_type test_##NAME(int); \
std::false_type test_##NAME(...); \
static_assert(decltype(test_##NAME(0))::value, \
"Expect { " #EXPR " } -> any_of {" #__VA_ARGS__ "}");
#define p std::declval<P &>()
#define cp std::declval<CP &>()
#define i std::size_t{}
#define x double{}
EXPECT(subscript, p[i], double &)
EXPECT(const_subscript, cp[i], double, const double, const double &)
EXPECT(evaluate, cp(x), double, const double)
EXPECT(negate, -cp, Polynomial, const Polynomial)
EXPECT(plus, cp + cp, Polynomial, const Polynomial)
EXPECT(minus, cp - cp, Polynomial, const Polynomial)
EXPECT(multiply, cp * cp, Polynomial, const Polynomial)
EXPECT(add_assign, p += cp, Polynomial &)
EXPECT(minus_assign, p -= cp, Polynomial &)
EXPECT(multiply_assign, p *= cp, Polynomial &)
EXPECT(derivative, cp.derivative(), Polynomial, const Polynomial)
EXPECT(integral, cp.integral(), Polynomial, const Polynomial)
EXPECT(equal, cp == cp, bool)
EXPECT(not_equal, cp != cp, bool)
#undef p
#undef cp
#undef i
#undef x
#undef EXPECT
还算能用吧。最好放进一个 namespace 里,避免造成名字空间污染。