mold 仓库内 oneTBB 的 blocked_nd_range CTAD 设计解析:从构造函数到 C++17 推导指引的完整实现

mold 仓库内 oneTBB 的 blocked_nd_range CTAD 设计解析:从构造函数到 C++17 推导指引的完整实现 mold 仓库内 oneTBB 的 blocked_nd_range CTAD 设计解析从构造函数到 C17 推导指引的完整实现【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本文以本仓库mold 项目其third-party/tbb内嵌 oneTBB 库中的设计文档 third-party/tbb/rfcs/supported/blocked_nd_range_ctad/README.md 为骨架系统讲解oneapi::tbb::blocked_nd_range在 C17 下启用类模板参数推导CTADClass Template Argument Deduction的完整设计四类构造函数的职责、五条显式推导指引的约束条件、以及单花括号初始化列表与单 C-array 场景下的歧义消解策略。读完本文你将能看懂 oneTBB 头文件中的推导指引源码并能在自己的并行代码中放心使用blocked_nd_range range(range1, range2, range3)这类免写模板参数的对象构造方式。背景blocked_nd_range 是什么oneapi::tbb::blocked_nd_range是 oneTBB 中用于表示可递归划分的 N 维区间的类模板其模板参数为Value, N其中Value是每一维上的值类型N是维度数量。它是 oneTBB 并行算法如parallel_for处理多维迭代空间时的核心 Range 类型之一。在启用 CTAD 之前创建这类对象必须显式写出全部模板参数oneapi::tbb::blocked_rangeint range1(0, 100); oneapi::tbb::blocked_rangeint range2(0, 200); oneapi::tbb::blocked_rangeint range3(0, 300); // 显式指定模板参数blocked_nd_rangeint, 3 oneapi::tbb::blocked_nd_rangeint, 3 nd_range(range1, range2, range3);CTADC17 起可用允许编译器根据构造函数实参自动推导模板参数。既然 3 个参数都是blocked_rangeint类型blocked_nd_rangeint, 3完全可以由编译器推导得出// 无需写出 int, 3类型自动推导为 oneapi::tbb::blocked_nd_rangeint, 3 oneapi::tbb::blocked_nd_range nd_range(range1, range2, range3);本设计文档即探讨如何为blocked_nd_range提供一套显式推导指引explicit deduction guides让 CTAD 在各种实参形态对象、数组、花括号初始化列表下都能正确、无歧义地工作。构造函数全集设计文档定义的四类构造入口设计文档首先明确了blocked_nd_range支持的构造函数集合类模板签名为template typename T, unsigned int Nusing value_type; // 每维的值类型 using dim_range_type blocked_rangevalue_type; // 单维区间类型 using size_type typename dim_range_type::size_type; blocked_nd_range(const dim_range_type dim0, /*恰好 N 个 const dim_range_type 参数*/); // [1] blocked_nd_range(const value_type (dim_size)[N], size_type grainsize 1); // [2] blocked_nd_range(blocked_nd_range r, split); // [3] blocked_nd_range(blocked_nd_range r, proportional_split proportion); // [4]构造函数 [1]由 N 个一维区间构成 N 维区间构造函数 [1] 是主构造路径通过提供 N 个blocked_range对象每个对象对应 N 维区间的一个维度。它同样支持用花括号初始化列表braced-init-list在调用现场直接构造每一维tbb::blocked_rangeint dim_range(0, 100); // 直接传入 blocked_range 对象 tbb::blocked_nd_rangeint, 2 nd_range_1(dim_range, tbb::blocked_rangeint(0, 200)); // 用花括号初始化列表原位构造两维{begin, end} 与 {begin, end, grainsize} tbb::blocked_nd_rangeint, 2 nd_range_2({0, 100}, {0, 200, 5}); // 混合方式一个 blocked_range 对象 一个花括号初始化列表 tbb::blocked_nd_rangeint, 2 nd_range_3(dim_range, {0, 200, 5});从源码实现看这一构造函数在 blocked_nd_range.h 中并不是手写的 N 参数版本而是借助make_index_sequenceN自动展开生成恰好 N 个tbb::blocked_rangeValue类型参数的构造函数templatetypename Value, unsigned int N, std::size_t... Is class blocked_nd_range_implValue, N, detail::index_sequenceIs... { public: using value_type Value; using dim_range_type tbb::blocked_rangevalue_type; using size_type typename dim_range_type::size_type; // 恰好 N 个 dim_range_type 参数由 index_sequence 展开而来 blocked_nd_range_impl(const indexed_tdim_range_type, Is... args) : my_dims{ {args...} } {} ... };头文件注释对此设计有明确说明blocked_nd_range_impl上方注释使用make_index_sequenceN自动生成恰好 N 个tbb::blocked_rangeValue参数的构造函数可以顺带支持用花括号初始化列表初始化每一维而形参表示为花括号初始化列表但又不是std::initializer_list或对其引用时会在模板实参推导中形成非推导上下文non-deduced context因此必须靠显式推导指引兜底。同时注释强调blocked_nd_range必须恰好是blocked_nd_range_impl的模板别名而非派生类否则需要自行声明构造函数并面临同样问题。注意最外层的blocked_nd_range只是带约束__TBB_requires(blocked_range_valueValue)见 detail/_range_common.h 中的 concept 定义的别名式派生类通过using base::base;继承构造templatetypename Value, unsigned int N __TBB_requires(blocked_range_valueValue) class blocked_nd_range : public blocked_nd_range_implValue, N { using base blocked_nd_range_implValue, N; using base::base; // 继承基类构造函数 };blocked_range_valueValue概念要求Value可拷贝且支持lhs rhs、lhs - rhs可转换为std::size_t、lhs (rhs - lhs)可转换为Value——这也解释了为何 [2] 构造允许Value是整数或迭代器类型。构造函数 [2]由 C 数组每元素为一维尺寸构造构造函数 [2] 接受一个 C 数组每个元素代表对应维度的尺寸每个维度区间均从 0 开始。可选的grainsize参数作用于所有维度// 数组对象本身 int sizes[3] {100, 200, 300}; // 构造 3 维区间 [0, 100)、[0, 200)、[0, 300) tbb::blocked_nd_rangeint, 3 nd_range_1(sizes); // 指定 grainsize每个维度区间的 grainsize 均为 5 tbb::blocked_nd_rangeint, 3 nd_range_2(sizes, 5); // 直接用花括号初始化列表代替栈上数组 tbb::blocked_nd_rangeint, 3 nd_range_3({100, 200, 300});源码中的实现核心是一行展开构造见 blocked_nd_range.hblocked_nd_range_impl(const value_type (size)[N], size_type grainsize 1) : my_dims { dim_range_type(0, size[Is], grainsize)... } {}即对第Is维生成dim_range_type(0, size[Is], grainsize)形成 N 个半开区间[0, size[i])。仓库还在 Clang 14.0.0 之前版本的兼容分支中加了enable_ifM N约束防止单花括号列表误匹配到该构造函数而产生编译错误。构造函数 [3] 与 [4]分裂构造Range 命名要求构造函数 [3]、[4] 用于把区间拆成两半是Range命名要求Named Requirements的组成部分供 oneTBB 并行算法内部使用[3]blocked_nd_range(blocked_nd_range r, split)等分分裂[4]blocked_nd_range(blocked_nd_range r, proportional_split proportion)按比例分裂。源码中的实现逻辑do_split见 blocked_nd_range.h是利用std::max_element按尺寸与 grainsize 的比值挑选出最值得分裂的维度比较条件为first.size() * double(second.grainsize()) second.size() * double(first.grainsize())再调用dim_range_type::do_split对该维执行分裂。这与测试 conformance_blocked_nd_range.cpp 中 blocked_nd_range proportional splitting 用例验证的行为一致按比例分裂时要么按行dim 0要么按列dim 1分裂且分裂点符合ps.left() * size / (ps.left() ps.right())的期望。推导指引设计五条显式指引 [g1]–[g5][g1]N 个同类型 blocked_range 对象的推导设计文档给出的指引 [g1] 对应构造函数 [1] 中直接传 N 个blocked_range对象的情形template typename Value, typename... Values blocked_nd_range(blocked_rangeValue, blocked_rangeValues...) - blocked_nd_rangeValue, 1 sizeof...(Values);它仅在Values...中所有类型都与Value相同时才参与重载决议。实际源码blocked_nd_range.h用 C17 折叠表达式实现了这一约束template typename Value, typename... Values, typename std::enable_if_t(... std::is_same_vValue, Values) blocked_nd_range(blocked_rangeValue, blocked_rangeValues...) - blocked_nd_rangeValue, 1 sizeof...(Values);对应测试test_deduction_guides验证了推导结果blocked_nd_range range(dim_range, dim_range, dim_range); static_assert(std::is_same_vdecltype(range), blocked_nd_rangeT, 3);[g2]多个 C 数组花括号初始化列表的推导为了覆盖把多个blocked_range以花括号初始化列表传入的情形需要一条接收一组 C 数组的推导指引。设计文档比较了两种候选形态——C 数组与std::initializer_list并指出std::initializer_list的问题在于无法在编译期跟踪元素个数因此采用 C 数组引用形态template typename Value, unsigned int... Ns blocked_nd_range(const Value (...)[Ns]) - blocked_nd_rangeValue, sizeof...(Ns);该指引仅在同时满足以下两个条件时参与重载决议提供的 C 数组数量sizeof...(Ns) 2每个 C 数组的大小只能是 2 或 3。约束 1 用于与 [1]/[2] 歧义消解见下文歧义小节约束 2 保证花括号初始化列表一定能用来初始化blocked_range——目前blocked_range只支持 2 参数构造begin、end和 3 参数构造begin、end、grainsize。仓库源码的实现与设计文档完全一致blocked_nd_range.htemplate typename Value, unsigned int... Ns, typename std::enable_if_tsizeof...(Ns) 2, typename std::enable_if_t(... (Ns 2 || Ns 3)) blocked_nd_range(const Value (... dim)[Ns]) - blocked_nd_rangeValue, sizeof...(Ns);重要限制指引 [g2] 要求花括号初始化列表中的所有元素类型一致。因此无法用它构造各维类型不同或grainsize 无法由区间值类型表达的blocked_rangestd::vectorint vector; // OK推导为 blocked_nd_rangeiterator, 1 blocked_nd_range range1({vector.begin(), vector.end()}); // FAIL花括号初始化列表内所有元素必须同类型。 // 迭代器类型不能转换为 size_type无法在列表内提供 grainsize 5 blocked_nd_range range({vector.begin(), vector.end(), /*grainsize */5});[g3]单个 C 数组构造函数 [2]的推导构造函数 [2] 对应的推导指引 [g3] 很简单——从单个 C 数组同时推导出值类型Value和数组长度Ntemplate typename Value, unsigned int N blocked_nd_range(const Value ()[N]) - blocked_nd_rangeValue, N;源码中该指引额外带上了默认参数类型以匹配构造函数 [2] 的可选 grainsize 参数template typename Value, unsigned int N blocked_nd_range(const Value ()[N], typename blocked_nd_rangeValue, N::size_type 1) - blocked_nd_rangeValue, N;[g4] 与 [g5]分裂构造的推导服务性构造函数 [3]、[4] 对应的两条推导指引template typename Value, unsigned int N blocked_nd_range(blocked_nd_rangeValue, N, split) - blocked_nd_rangeValue, N; template typename Value, unsigned int N blocked_nd_range(blocked_nd_rangeValue, N, proportional_split) - blocked_nd_rangeValue, N;设计文档特别指出从规范spec角度看这类指引本可以作为隐式推导指引implicit deduction guides自动生成与拷贝/移动构造函数同理但当前 oneTBB 实现中并未隐式生成因此必须显式写出。指引 [g4]、[g5] 不属于规范的一部分仅属于 oneTBB 的实现细节。测试用例对这两条指引及拷贝/移动场景一并做了验证conformance_blocked_nd_range.cppblocked_nd_range range(source_range, oneapi::tbb::split{}); static_assert(std::is_same_vdecltype(range), decltype(source_range)); blocked_nd_range range(source_range, oneapi::tbb::proportional_split{1, 3}); static_assert(std::is_same_vdecltype(range), decltype(source_range)); blocked_nd_range range(source_range); // 拷贝 static_assert(std::is_same_vdecltype(range), decltype(source_range)); blocked_nd_range range(std::move(source_range)); // 移动 static_assert(std::is_same_vdecltype(range), decltype(source_range));所有推导指引都被包在#if __TBB_CPP17_DEDUCTION_GUIDES_PRESENT预处理宏内即仅在编译器支持 C17 推导指引时启用对应的测试也以同一宏为前置条件。设计结论三类歧义场景的消解策略歧义 1单个大小为 2 或 3 的花括号初始化列表使用 CTAD 时单个大小为 2 或 3 的花括号初始化列表存在两种可能的解读blocked_nd_range range1({10, 20}); blocked_nd_range range2({10, 20, 5});由于模板参数未显式指定编译器面临两种候选解释一维区间[10, 20)grainsize 为 1 或 5应推导为blocked_nd_rangeint, 1走构造函数 [1]多维区间[0, 10)、[0, 20)以及[0, 5)应推导为blocked_nd_rangeint, 3走构造函数 [2]。当前 oneTBB 实现优先选择第二种多维解释理由有二多维区间在实际使用中更常见、更实用同时要与下面单个 C 数组场景的行为保持一致。歧义 2单个大小为 2 或 3 的 C 数组对象类似的歧义出现在把单个 C 数组对象传给构造函数时int array[2] {100, 200}; tbb::blocked_nd_range range(array);由于blocked_range不能从 C 数组构造且这里没有使用花括号初始化列表用户预期它被推导为blocked_nd_rangeint, 2并走构造函数 [2]。当前 oneTBB 实现正是通过约束 [g2] 至少接收 2 个及以上初始化列表来保证这一行为单 C 数组或单花括号列表只能命中 [g3]从而恒定为多维语义歧义 1 中的两种候选也因此在单列表场景下不复存在。歧义 3构造函数 [1] 的混合实参已知限制最后一种情况是构造函数 [1] 同时混用精确的tbb::blocked_range对象与花括号初始化列表tbb::blocked_rangeint dim_range(0, 100); tbb::blocked_nd_range nd_range(dim_range, {0, 200}, {0, 300}, dim_range);这类实参既无法匹配 [g1]其中有花括号列表不是blocked_range对象也无法匹配 [g2]其中有blocked_range对象不是 C 数组。设计文档明确指出目前尚不清楚如何定义一条能覆盖这种混合情形的推导指引这是 CTAD 支持上的一个已知限制。若确有此类需求仍需显式写出模板参数如blocked_nd_rangeint, 4。测试矩阵推导指引的完整验证仓库在 conformance_blocked_nd_range.cpp 的test_deduction_guidesT()中为int与自定义类型fancy_value各跑一遍全矩阵验证。fancy_value是刻意构造的行为受限类型——只提供operator、operator-、operator和到std::size_t的转换恰好满足blocked_range_value概念的最低要求用于验证推导指引不依赖具体内置类型。测试覆盖的关键推导结果均以static_assert(std::is_same_v...)断言实参形态推导结果(dim_range, dim_range, dim_range)三个对象blocked_nd_rangeT, 3({v.begin(), v.end()}, {v.begin(), v.end()})两个列表blocked_nd_rangeiterator, 24 个混合 2/3 元素列表blocked_nd_rangeT, 4({T{100}})单个 1 元素列表blocked_nd_rangeT, 1(array[1])、({T{100}}, 5)、(array[1], 5)blocked_nd_rangeT, 1({T{100}, T{200}})、({T{100}, T{200}}, 5)、(array[2], 5)blocked_nd_rangeT, 2({T{100}, T{200}, T{300}})及带 grainsize 变体、(array[3], 5)blocked_nd_rangeT, 3({T{100}, T{200}, T{300}, T{400}})、(array[4])等blocked_nd_rangeT, 4(source_range, split{})/(source_range, proportional_split{1, 3})与source_range同类型这组测试同时印证了三条设计结论单列表按 1 维处理还是按多维处理取决于元素个数1 个元素 → 1 维2/3/4 个元素 → 相应维数单 C 数组始终按 [2] 多维语义推导分裂与拷贝/移动构造保持类型不变。结语如何在你的代码中使用 CTAD启用 CTAD 后blocked_nd_range的使用可以大幅简化// 传统写法显式模板参数 oneapi::tbb::blocked_nd_rangeint, 3 r1({0, 100}, {0, 200}, {0, 300}); // CTAD 写法类型自动推导为 blocked_nd_rangeint, 3 oneapi::tbb::blocked_nd_range r2({0, 100}, {0, 200}, {0, 300});实用要点回顾传入N 个同类型blocked_range对象推导为 N 维[g1]传入2 个及以上花括号初始化列表每个含 2 或 3 个同类型元素推导为列表个数维[g2]传入单个 C 数组或单个列表按多维语义推导[g3]元素个数即维度花括号初始化列表内所有元素必须同类型因此需要区间值类型无法表达 grainsize如迭代器场景时请显式写出模板参数混合传入blocked_range对象与花括号列表目前无法通过 CTAD 推导是已知限制需要显式指定Value, N。本设计文档收录于 third-party/tbb/rfcs/supported/blocked_nd_range_ctad/README.md。按 supported 目录说明rfcs/supported下的提案描述的是已实现并作为 oneTBB 完全支持特性发布的功能实现质量高、会持续测试因此本文所描述的行为可以直接信赖。配套的完整实现见 blocked_nd_range.h底层blocked_range见 blocked_range.h一致性测试见 conformance_blocked_nd_range.cpp类型约束概念见 detail/_range_common.h读者可据此深入研读。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考