C++多参数Concept作为函数约束的类型推导问题探讨
一、先定义基础Concept
首先把标量约束为任意算术类型:
template<class T> concept scalar = std::is_arithmetic_v<T>;
接着定义基于标量ScalarType的向量概念,要求支持加减、数乘、取反等运算:
template<class T, class ScalarType> concept vector = std::equality_comparable<T> && scalar<ScalarType> && requires(T a, T b, ScalarType c) { {a + b} -> std::same_as<T>; {a - b} -> std::same_as<T>; {a += b} -> std::same_as<T&>; {a -= b} -> std::same_as<T&>; {c * a} -> std::same_as<T>; {a * c} -> std::same_as<T>; {a / c} -> std::same_as<T>; {a *= c} -> std::same_as<T&>; {a /= c} -> std::same_as<T&>; {-a} -> std::same_as<T>; {zero(std::declval<empty<T>>())} -> std::same_as<T>; };
最后定义点概念,要求点能和向量做加减,两点相减得到向量:
template<class T, class VectorType, class ScalarType> concept point = std::equality_comparable<T> && vector<VectorType, ScalarType> && requires(T p1, T p2, VectorType v) { {p1 + v} -> std::same_as<T>; {p1 - v} -> std::same_as<T>; {p1 += v} -> std::same_as<T&>; {p1 -= v} -> std::same_as<T&>; {p1 - p2} -> std::same_as<VectorType>; };
二、距离函数的推导坑
用point约束写了个两点距离计算函数:
template<class Vector, class Scalar, point<Vector, Scalar> P> constexpr auto distance(P p1, P p2) { return norm(p1 - p2); }
结果发现只传点类型P的时候,编译器推不出Vector和Scalar参数。比如非void指针符合point概念:
static_assert(point<int*, intptr_t, intptr_t>);
调用时直接报错:
constexpr std::array<int, 20> test{}; auto d = ::distance(std::data(test), std::data(test) + std::size(test));
错误信息:
couldn’t deduce template parameter ‘Vector’
手动指定模板参数倒是能跑:
auto d = distance<intptr_t, intptr_t>(std::data(test), std::data(test) + std::size(test));
三、问题与解决办法
核心疑问
能不能保留Vector和Scalar作为自由类型参数,同时让模板自动推导?还是必须要求点/向量类型自带可识别的类型别名(比如下面改的point Concept),但这样会限制兼容性——我们还想支持std::chrono这类仿射空间场景呢。
改写后的point Concept(依赖类型别名):
template<class T> concept point = std::equality_comparable<T> && requires(T p1, T p2) { typename T::vector_type; {p1 + v} -> std::same_as<T>; {p1 - v} -> std::same_as<T>; {p1 += v} -> std::same_as<T&>; {p1 -= v} -> std::same_as<T&>; {p1 - p2} -> std::same_as<T::vector_type>; };
比如适配std::chrono:
static_assert(point<std::chrono::steady_clock::time_point, std::chrono::steady_clock::duration, long>);
不用类型别名的解决思路
不需要强制类型别名,通过从点的运算结果反向推导向量和标量类型就能实现自动推导:
方案1:调整Concept参数顺序,默认推导
把point Concept的点类型放在第一个,向量和标量类型设为默认参数,从点的运算结果推导:
// 辅助模板:从点类型获取向量类型 template<class P> using vector_from_point = decltype(std::declval<P>() - std::declval<P>()); // 辅助模板:从向量类型推导标量类型 template<class V> struct scalar_from_vector { template<class S> static auto test(int) -> std::enable_if_t<vector<V, S>, S>; template<class S> static auto test(...) -> void; using type = decltype(test<scalar>(0)); }; template<class V> using scalar_from_vector_t = typename scalar_from_vector<V>::type; // 调整后的point概念 template<class T, class VectorType = vector_from_point<T>, class ScalarType = scalar_from_vector_t<VectorType>> concept point = std::equality_comparable<T> && vector<VectorType, ScalarType> && requires(T p1, T p2, VectorType v) { {p1 + v} -> std::same_as<T>; {p1 - v} -> std::same_as<T>; {p1 += v} -> std::same_as<T&>; {p1 -= v} -> std::same_as<T&>; {p1 - p2} -> std::same_as<VectorType>; };
这样定义distance函数时,只需要指定点类型参数,编译器会自动推导出向量和标量:
template<point P> constexpr auto distance(P p1, P p2) { return norm(p1 - p2); }
方案2:直接在函数约束里推导
如果不想改原来的point Concept,也可以在distance函数的约束里直接推导向量和标量类型:
// 复用上面的scalar_from_vector_t辅助模板 template<class V> struct scalar_from_vector { template<class S> static auto test(int) -> std::enable_if_t<vector<V, S>, S>; template<class S> static auto test(...) -> void; using type = decltype(test<scalar>(0)); }; template<class V> using scalar_from_vector_t = typename scalar_from_vector<V>::type; template<class P> constexpr auto distance(P p1, P p2) { using Vector = decltype(p1 - p2); using Scalar = scalar_from_vector_t<Vector>; static_assert(point<P, Vector, Scalar>); return norm(p1 - p2); }
这两种方式都不用强制类型别名,既能保留Vector和Scalar作为自由参数,又能让模板自动推导,同时兼容指针、std::chrono::time_point这类没有内置类型别名的类型。
内容的提问来源于stack exchange,提问作者user877329

