C++20 Concepts如何约束map方法参数返回值满足forward_range要求
问题描述
假设我需要指定某类型的对象必须存在map(...)方法,该方法可将一个forward_range映射为另一个(类型可不同的)forward_range,该如何通过C++20 concept编写对应的约束?
例如下方的结构体应当满足该约束:
struct Mapper{ std::vector<double> map(std::list<int> args); };
我此前查阅过如何通过C++ Concepts约束类中存在指定函数的相关资料,但不清楚如何对方法的参数、返回类型应用概念约束,而非将其限定为具体类型。
补充说明
我后续又查阅了C++20 requires表达式中使用未指定类型的相关问题,但仍无法解决当前场景的问题,具体需求如下:
我需要实现一组作用于映射器的操作符,代码如下:
template<Map T1, Map T2> class Chain { const T1& map1; const T2& map2; public: Chain(const T1& map1, const T2& map2) :map1{ map1 }, map2{ map2 } {} template<forward_range T> auto map(T input) const { return map2(map1(input)); } }; template<Map T1, Map T2> auto operator| (const T1& map1, const T2& map2) { return Chain(map1, map2); }
我需要让上述operator|仅作用于满足以下要求的对象:其拥有的map函数入参为某类forward_range,返回值也为某类forward_range。
经调研我认为这种用法有点将concept作为接口使用,这类场景原本应当通过定义Map基类实现,但我既不知道如何在基类中为map方法定义这类泛型要求,也希望学习concept的使用方法。
需要注意该需求存在参数要求强度反转的概念问题:如果约束语义为接收拥有可接受任意forward_range入参、返回任意forward_range的map方法的对象,这一要求过于严格;我实际不需要map方法能够处理所有类型的forward_range,仅要求其接受的入参类型本身属于forward_range即可。
回答
核心需要先明确:concept是静态编译期检查,你可以根据自己需要的约束强度选择对应实现,三种常用方案如下。
1. 弱约束:匹配固定签名的map方法(适配给出的Mapper示例)
如果你的map是普通非模板、非重载的成员函数,仅要求其形参类型和返回类型都满足forward_range,可以通过成员函数指针提取签名做检查。
首先引入必要头文件:
#include <ranges> #include <concepts> #include <type_traits>
先写辅助trait提取成员函数的参数、返回类型:
// 提取非const成员函数签名 template <typename F> struct map_sig; template <typename Ret, typename Class, typename Arg> struct map_sig<Ret(Class::*)(Arg)> { using arg_t = std::remove_cvref_t<Arg>; using ret_t = Ret; }; // 提取const成员函数签名 template <typename Ret, typename Class, typename Arg> struct map_sig<Ret(Class::*)(Arg) const> { using arg_t = std::remove_cvref_t<Arg>; using ret_t = Ret; };
然后编写concept:
template <typename T> concept Map = requires { typename map_sig<decltype(&T::map)>::arg_t; typename map_sig<decltype(&T::map)>::ret_t; requires std::ranges::forward_range<typename map_sig<decltype(&T::map)>::arg_t>; requires std::ranges::forward_range<typename map_sig<decltype(&T::map)>::ret_t>; };
这个concept可以正确匹配给出的Mapper结构体:&Mapper::map的签名是std::vector<double>(Mapper::*)(std::list<int>),其中std::list<int>和std::vector<double>都满足forward_range。
注意:该写法不支持重载的
map方法、模板成员函数map,因为这类场景下decltype(&T::map)无法确定唯一地址,会编译失败。
2. 调用点约束:适配重载/模板map,不要求支持所有forward_range
你写的Chain::map本身是接收任意forward_range的模板,实际上不需要在operator|阶段就强校验mapper的通用性,只需要在实际调用map方法时,检查传入的范围类型可以被当前mapper处理即可,这也是更符合C++ concept设计思路的写法。
首先定义和输入范围绑定的mapper约束:
template <typename M, typename InputRange> concept MapperFor = std::ranges::forward_range<InputRange> && requires(M m, InputRange r) { { m.map(std::move(r)) } -> std::ranges::forward_range; };
然后修改Chain和operator|的实现,把约束放到调用点:
template <typename T1, typename T2> class Chain { const T1& map1; const T2& map2; public: Chain(const T1& map1, const T2& map2) :map1{map1}, map2{map2} {} template <std::ranges::forward_range T> requires MapperFor<T1, T> && MapperFor<T2, decltype(std::declval<T1&>().map(std::declval<T>()))> auto map(T input) const { return map2.map(map1.map(std::move(input))); } }; template <typename T1, typename T2> auto operator|(const T1& map1, const T2& map2) { return Chain<T1, T2>(map1, map2); }
这种写法的优势:
- 完全兼容给出的固定签名
Mapper示例,只要传入的输入范围是std::list<int>即可正常编译 - 支持重载的
map方法、模板成员函数map - 不要求mapper处理所有
forward_range,只要能处理实际传入的类型即可 - 传入不支持的范围类型时会给出清晰的约束不满足编译错误
- 修正了原代码的错误:
map是成员方法,需要显式调用,同时用std::move避免范围的不必要拷贝
3. 强约束:要求map支持任意forward_range
如果你确实需要mapper的map方法可以处理所有forward_range输入(比如做通用的范围适配器),可以直接用requires表达式的全称量化约束:
template <typename M> concept Map = requires(M m, std::ranges::forward_range auto input) { { m.map(input) } -> std::ranges::forward_range; };
这个约束要求M::map必须是模板成员函数,或者形参可以接收任意forward_range(比如通过范围视图的通用类型擦除包装),示例实现:
struct Mapper { template <std::ranges::forward_range R> auto map(R args) { return args | std::views::transform([](const auto& x) { return static_cast<double>(x); }) | std::ranges::to<std::vector>(); } };
这种写法下所有满足Map约束的类型都可以任意链式组合,处理任意输入forward_range。
内容的提问来源于stack exchange,提问作者mqnc

