如何用C++17的type_traits约束兼容范围for循环的类型?
问题
现有如下模板类,使用了“可迭代”类型:
template <typename Iterable> struct foo { void bar(Iterable& collection) { for (const auto& item : collection) { // ... } } };
能否通过<type_traits>约束Iterable类型,使其仅接受兼容范围for循环的类型?比如像这样:
template <typename Iterable, std::enable_if<std::is_range_for_iterable_v<Iterable>> * = nullptr>
cppreference的type_traits列表里没有std::is_range_for_iterable这类特性。我查了兼容范围for循环的定义:
仅作说明用的表达式/* begin-expr /和/ end-expr */定义如下:
- 若/* range */的类型是数组类型R的引用:
- 若R是有界数组(大小为N),则/* begin-expr /为/ range /,/ end-expr /为/ range */ + N。
- 若R是未知边界或不完全类型的数组,程序将非法。
- 若/* range /的类型是类类型C的引用,且在C的作用域中查找begin和end各至少找到一个声明,则/ begin-expr /为/ range /.begin(),/ end-expr /为/ range */.end()。
- 否则,/* begin-expr /为begin(/ range /),/ end-expr /为end(/ range */),其中begin和end通过参数依赖查找(ADL)找到(不执行非ADL查找)。
我尝试用is_array处理数组情况,用is_invocable等特性查找匹配的begin()和end()函数,但担心实现复杂且有边界情况(误判兼容/不兼容场景)。有没有更简单或被广泛认可的实现方式?注:C20的concepts有更优雅的方案,但我需要基于C17的type_traits实现。
基于C++17的实现方案
可以通过SFINAE结合自定义类型特性来实现,核心是模拟范围for循环的三个判定逻辑,同时规避边界情况。以下是一种被广泛认可的实现方式:
1. 核心辅助特性定义
首先实现一个is_range特性,分步骤匹配范围for的规则:
#include <type_traits> #include <utility> // 辅助ADL查找begin/end的命名空间 namespace adl_helper { using std::begin; using std::end; template <typename T> auto adl_begin(T&& t) -> decltype(begin(std::forward<T>(t))); template <typename T> auto adl_end(T&& t) -> decltype(end(std::forward<T>(t))); } // 检查是否存在可调用的成员begin/end template <typename T, typename = void> struct has_member_begin_end : std::false_type {}; template <typename T> struct has_member_begin_end<T, std::void_t<decltype(std::declval<T&>().begin()), decltype(std::declval<T&>().end())>> : std::true_type {}; // 检查是否能通过ADL找到begin/end template <typename T, typename = void> struct has_adl_begin_end : std::false_type {}; template <typename T> struct has_adl_begin_end<T, std::void_t<decltype(adl_helper::adl_begin(std::declval<T&>())), decltype(adl_helper::adl_end(std::declval<T&>()))>> : std::true_type {}; // 主特性:判断是否兼容范围for循环 template <typename T> struct is_range_for_iterable : std::disjunction< std::is_array<std::remove_reference_t<T>>, // 匹配有界数组(未知边界数组会在实例化时报错) has_member_begin_end<T>, has_adl_begin_end<T> >::type {}; template <typename T> constexpr bool is_range_for_iterable_v = is_range_for_iterable<T>::value;
2. 约束模板类
现在可以用这个特性约束你的foo类:
template <typename Iterable, std::enable_if_t<is_range_for_iterable_v<Iterable>>* = nullptr> struct foo { void bar(Iterable& collection) { for (const auto& item : collection) { // ... } } };
3. 关键细节说明
- 数组处理:
std::is_array会匹配所有数组类型,未知边界数组在实例化foo时会触发编译错误(符合标准中“程序非法”的规定),无需额外过滤。 - 成员begin/end检查:用
std::void_t检测类型是否拥有可调用的begin()和end()成员函数,确保类型符合范围for的第二类判定规则。 - ADL查找模拟:通过
adl_helper命名空间引入std::begin和std::end,模拟标准中“仅ADL查找”的规则——如果ADL找不到,才会 fallback 到std的版本,完全贴合范围for的判定逻辑。 - 优先级匹配:
std::disjunction会按顺序匹配条件,优先匹配数组和成员函数的情况,最后才走ADL,和范围for的判定顺序完全一致,避免误判。
4. 测试场景验证
这个实现可以正确处理:
- 标准容器(
std::vector、std::list等,拥有成员begin/end) - C风格有界数组(
int arr[5]) - 自定义可迭代类型(拥有成员
begin/end,或通过ADL提供begin/end) - 自动排除无迭代能力的类型(比如
int、std::nullptr_t等)
内容的提问来源于stack exchange,提问作者TypeIA
相关产品推荐
相关产品推荐

