mold 内置 TBB 详解:concurrent_unordered_map 的 C++17 推导指引(Deduction Guides)
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本文基于 mold 仓库中内置的 TBB(oneTBB)第三方组件,围绕容器规范文档 deduction_guides.rst 展开:系统讲解concurrent_unordered_map构造函数对 C++17 类模板参数推导(CTAD)的支持方式,包括隐式推导指引、七组显式推导指引的完整签名、类型别名定义与参与重载决议的前提条件,并结合 concurrent_unordered_map.h 的实际源码印证每条推导指引的真实实现与 SFINAE 约束。读完后,你可以直接在不写模板参数的情况下正确推导concurrent_unordered_map的完整类型,并理解推导失败的边界条件。
背景:concurrent_unordered_map 的模板参数与推导动机
concurrent_unordered_map定义于 concurrent_unordered_map.h,其模板参数共有 5 个:
template <typename Key, typename T, typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>, typename Allocator = tbb::tbb_allocator<std::pair<const Key, T>>> class concurrent_unordered_map : public concurrent_unordered_base<concurrent_unordered_map_traits<Key, T, Hash, KeyEqual, Allocator, false>>;完整写法concurrent_unordered_map<int, float>之外的参数大多有默认值,但在实际使用时经常需要指定自定义Hash或Allocator,完整写出所有参数会很冗长。C++17 引入的类模板参数推导(class template argument deduction,CTAD)允许编译器从构造函数的实参类型反推出类模板参数,这正是该规范文档的主题:concurrent_unordered_map的构造函数在可能的情况下支持 CTAD。
从源码结构看,整个容器通过继承concurrent_unordered_base(实现位于 detail/_concurrent_unordered_base.h)来复用范围构造、初始化列表构造等逻辑,例如范围构造器(_concurrent_unordered_base.h):
template <typename InputIterator> concurrent_unordered_base( InputIterator first, InputIterator last, size_type bucket_count = initial_bucket_count, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type() ) : concurrent_unordered_base(bucket_count, hash, equal, alloc) { insert(first, last); }推导指引推导出的类型,最终都会落到这一组构造函数上。文档中反复出现的map_size_type参数,指的就是被推导出的concurrent_unordered_map的size_type成员类型。
隐式推导指引:拷贝与移动构造函数
文档第一要点:拷贝构造函数和移动构造函数——包括显式带allocator_type参数的那些——提供由编译器隐式生成的推导指引。
这一点在源码中有明确标注,concurrent_unordered_map.h:
// Required for implicit deduction guides concurrent_unordered_map() = default; concurrent_unordered_map( const concurrent_unordered_map& ) = default; concurrent_unordered_map( const concurrent_unordered_map& other, const allocator_type& alloc ) : base_type(other, alloc) {} concurrent_unordered_map( concurrent_unordered_map&& ) = default; concurrent_unordered_map( concurrent_unordered_map&& other, const allocator_type& alloc ) : base_type(std::move(other), alloc) {}也就是说,concurrent_unordered_map m2(m1);、concurrent_unordered_map m3(std::move(m1), alloc);这类写法都能借助隐式推导指引得到与实参相同的完整模板参数,无需手写类型。注释// Required for implicit deduction guides说明这些默认化的特殊成员函数是刻意保留的,目的就是让编译器能据此生成隐式推导指引。
显式推导指引:七组完整签名
在隐式指引之外,文档列出了 7 条显式推导指引,覆盖“迭代器范围构造”和“初始化列表构造”两大入口,按参数个数(1 个/2 个/5 个参数)细分。以下完整保留文档中的签名(map_size_type为被推导类型的size_type):
1. 迭代器范围 + 全部可选参数(默认 Hash / KeyEqual / Allocator)
template <typename InputIterator, typename Hash = std::hash<iterator_key_t<InputIterator>>, typename KeyEqual = std::equal_to<iterator_key_t<InputIterator>>, typename Allocator = tbb::tbb_allocator<iterator_alloc_value_t<InputIterator>>> concurrent_unordered_map( InputIterator, InputIterator, map_size_type = {}, Hash = Hash(), KeyEqual = KeyEqual(), Allocator = Allocator() ) -> concurrent_unordered_map<iterator_key_t<InputIterator>, iterator_mapped_t<InputIterator>, Hash, KeyEqual, Allocator>;这是最常用的一条:仅给两个迭代器,即可从value_type::first_type(去 const)与value_type::second_type推导出Key和T,Hash、KeyEqual、Allocator全部取默认值。
2. 迭代器范围 + 指定 Hash 和 Allocator(KeyEqual 固定为 equal_to)
template <typename InputIterator, typename Hash, typename Allocator> concurrent_unordered_map( InputIterator, InputIterator, map_size_type, Hash, Allocator ) -> concurrent_unordered_map<iterator_key_t<InputIterator>, iterator_mapped_t<InputIterator>, Hash, std::equal_to<iterator_key_t<InputIterator>>, Allocator>;3. 迭代器范围 + 只指定 Allocator
template <typename InputIterator, typename Allocator> concurrent_unordered_map( InputIterator, InputIterator, map_size_type, Allocator ) -> concurrent_unordered_map<iterator_key_t<InputIterator>, iterator_mapped_t<InputIterator>, std::hash<iterator_key_t<InputIterator>>, std::equal_to<iterator_key_t<InputIterator>>, Allocator>;4. 初始化列表 + 全部可选参数(Key 去 const)
template <typename Key, typename T, typename Hash = std::hash<std::remove_const_t<Key>>, typename KeyEqual = std::equal_to<std::remove_const_t<Key>>, typename Allocator = tbb::tbb_allocator<std::pair<const Key, T>>> concurrent_unordered_map( std::initializer_list<std::pair<Key, T>>, map_size_type = {}, Hash = Hash(), KeyEqual = KeyEqual(), Allocator = Allocator() ) -> concurrent_unordered_map<std::remove_const_t<Key>, T, Hash, KeyEqual, Allocator>;注意初始化列表形式中 Key 的推导是std::remove_const_t<Key>:因为std::pair<const Key, T>中首元素天然带 const,推导时要把这层 const 剥掉,保证与类模板参数Key的语义一致。
5. 初始化列表 + map_size_type + Allocator
template <typename Key, typename T, typename Allocator> concurrent_unordered_map( std::initializer_list<std::pair<Key, T>>, map_size_type, Allocator ) -> concurrent_unordered_map<std::remove_const_t<Key>, T, std::hash<std::remove_const_t<Key>>, std::equal_to<std::remove_const_t<Key>>, Allocator>;6. 初始化列表 + 仅 Allocator
template <typename Key, typename T, typename Allocator> concurrent_unordered_map( std::initializer_list<std::pair<Key, T>>, Allocator ) -> concurrent_unordered_map<std::remove_const_t<Key>, T, std::hash<std::remove_const_t<Key>>, std::equal_to<std::remove_const_t<Key>>, Allocator>;7. 初始化列表 + map_size_type + Hash + Allocator
template <typename Key, typename T, typename Hash, typename Allocator> concurrent_unordered_map( std::initializer_list<std::pair<Key, T>>, map_size_type, Hash, Allocator ) -> concurrent_unordered_map<std::remove_const_t<Key>, T, Hash, std::equal_to<std::remove_const_t<Key>>, Allocator>;类型别名:iterator_key_t、iterator_mapped_t、iterator_alloc_value_t
文档给出的三个辅助类型别名为:
template <typename InputIterator> using iterator_key_t = std::remove_const_t<typename std::iterator_traits<InputIterator>::value_type::first_type>; template <typename InputIterator> using iterator_mapped_t = typename std::iterator_traits<InputIterator>::value_type::second_type; template <typename InputIterator> using iterator_alloc_value_t = std::pair<std::add_const_t<iterator_key_t<InputIterator>, iterator_mapped_t<InputIterator>>>;即:Key 取迭代器value_type(即std::pair)的first_type并去除 const;mapped 取second_type;分配器所管理的值类型为“const Key + mapped”组成的 pair,与类默认分配器tbb::tbb_allocator<std::pair<const Key, T>>保持一致。
在仓库源码中,这三个别名实际定义在 detail/_template_helpers.h,其中第三个别名的名称是iterator_alloc_pair_t(规范文档中写作iterator_alloc_value_t,二者语义完全相同):
template <typename Iterator> using iterator_value_t = typename std::iterator_traits<Iterator>::value_type; template <typename Iterator> using iterator_key_t = typename std::remove_const<typename iterator_value_t<Iterator>::first_type>::type; template <typename Iterator> using iterator_mapped_t = typename iterator_value_t<Iterator>::second_type; template <typename Iterator> using iterator_alloc_pair_t = std::pair<typename std::add_const<iterator_key_t<Iterator>>::type, iterator_mapped_t<Iterator>>;这一实现细节也解释了为什么“给迭代器范围指定了 Allocator”与“使用默认 Allocator”会走不同的推导指引:默认分配器必须精确匹配推导出的std::pair<const Key, T>值类型,而用户自定义分配器的值类型不受此约束,因此源码将二者拆成独立指引以便 SFINAE 区分。
参与重载决议的前提条件
文档明确了这些推导指引参与重载决议的四项要求:
InputIterator满足 ISO C++ 标准 [input.iterators] 一节描述的 InputIterator 要求;Allocator满足 [allocator.requirements] 一节描述的 Allocator 要求;Hash类型不满足Allocator 要求;KeyEqual类型不满足Allocator 要求。
后两条要求的目的,从源码结构看是为了消除歧义:Hash/KeyEqual参数与Allocator参数在推导指引中的位置相近,如果某个自定义类型既像 Hasher 又像 Allocator,两条指引将同时可推导。源码用 SFINAE 把这些约束落实在每条推导指引上,例如 concurrent_unordered_map.h:
template <typename It, typename Hash = std::hash<iterator_key_t<It>>, typename KeyEq = std::equal_to<iterator_key_t<It>>, typename Alloc = tbb::tbb_allocator<iterator_alloc_pair_t<It>>, typename = std::enable_if_t<is_input_iterator_v<It>>, typename = std::enable_if_t<is_allocator_v<Alloc>>, typename = std::enable_if_t<!is_allocator_v<Hash>>, typename = std::enable_if_t<!is_allocator_v<KeyEq>>, typename = std::enable_if_t<!std::is_integral_v<Hash>>> concurrent_unordered_map( It, It, std::size_t = {}, Hash = Hash(), KeyEq = KeyEq(), Alloc = Alloc() ) -> concurrent_unordered_map<iterator_key_t<It>, iterator_mapped_t<It>, Hash, KeyEq, Alloc>;其中is_input_iterator_v与is_allocator_v是特性探测(trait detection),定义于 detail/_template_helpers.h:is_allocator依次探测类型是否具备value_type、可调用allocate、可调用deallocate,三项都满足才判定为 Allocator。
此外源码比规范文档多了一层保险——!std::is_integral_v<Hash>:如果第三个实参是整数字面量(例如误把桶数写在 Hash 的位置),推导会因 SFINAE 失败而被剔除,从而把错误导向更清晰的诊断。文档中map_size_type = {}这一可省参数,在源码中对应std::size_t = {},即桶数bucket_count,传入后会传给concurrent_unordered_base的构造器并执行round_up_to_power_of_two(见 _concurrent_unordered_base.h)。
所有显式推导指引都被#if __TBB_CPP17_DEDUCTION_GUIDES_PRESENT宏包裹(concurrent_unordered_map.h),保证在不支持 C++17 推导指引的编译器下整段静默剔除,容器本身仍可显式写模板参数使用。
实战示例:不写类型参数构造容器
文档给出的官方示例如下,可直接复制运行(需链接 TBB 库):
#include <oneapi/tbb/concurrent_unordered_map.h> #include <vector> #include <functional> struct CustomHasher {...}; int main() { std::vector<std::pair<int, float>> v; // 推导 m1 为 concurrent_unordered_map<int, float> oneapi::tbb::concurrent_unordered_map m1(v.begin(), v.end()); // 推导 m2 为 concurrent_unordered_map<int, float, CustomHasher>; oneapi::tbb::concurrent_unordered_map m2(v.begin(), v.end(), CustomHasher{}); }逐条对应推导过程:
m1命中第 1 条推导指引:迭代器value_type为std::pair<int, float>,故 Key 推导为int、mapped 推导为float,Hash/KeyEqual/Allocator全部取默认模板参数;m2传入自定义CustomHasher{}后,第三个实参不再是std::size_t,编译器在可推导的指引中选择 Hash 参数槽位能接住CustomHasher的那一条(前提是CustomHasher不满足 Allocator 要求,且非整型),最终得到concurrent_unordered_map<int, float, CustomHasher>;- 初始化列表形式同理,例如
concurrent_unordered_map m{{1, 1.0f}, {2, 2.0f}};会按第 4 条指引推导为concurrent_unordered_map<int, float>。
一个值得注意的边界:源码在 concurrent_unordered_map.h 留有// TODO: investigate if a deduction guide for concurrent_unordered_map(It, It, Alloc) is needed的注释,说明“迭代器范围 + 仅分配器(不带桶数)”这一组合当前没有对应的显式推导指引,使用时应显式带上桶数或改用显式模板参数。
源码中的实现细节补充
阅读 concurrent_unordered_map.h 时还有两处与文档互补的细节:
编译器缺陷规避:源码末尾(concurrent_unordered_map.h)专门为 Apple LLVM 10.0.0 增加了一条显式推导指引:
#if __APPLE__ && __TBB_CLANG_VERSION == 100000 // An explicit deduction guide is required for copy/move constructor with allocator for APPLE LLVM 10.0.0 template <typename Key, typename T, typename Hash, typename KeyEq, typename Alloc> concurrent_unordered_map( concurrent_unordered_map<Key, T, Hash, KeyEq, Alloc>, Alloc ) -> concurrent_unordered_map<Key, T, Hash, KeyEq, Alloc>; #endif注释说明该版本编译器在特定条件下无法为“带 allocator 的拷贝/移动构造”生成隐式推导指引,因此用显式指引兜底。这从侧面印证了文档中“拷贝/移动构造提供隐式推导指引”一句的实际工程价值。
兄弟容器的同款设计:
concurrent_hash_map、concurrent_map、concurrent_set等容器的推导指引采用完全相同的结构(见 concurrent_hash_map.h、concurrent_map.h),即“范围/初始化列表构造 + SFINAE 探测迭代器与分配器”,理解本文的推导机制后可以平移到这些容器上。
小结
concurrent_unordered_map的 CTAD 支持可以归纳为一张速查表:
| 构造入口 | 可推导的参数 | 默认补齐的参数 |
|---|---|---|
| 迭代器范围(仅首尾迭代器) | Key、T | Hash / KeyEqual / Allocator 全部默认 |
| 迭代器范围 + 桶数 + Hash + Allocator | Key、T | KeyEqual =std::equal_to<Key> |
| 迭代器范围 + 桶数 + Allocator | Key、T | Hash / KeyEqual 默认 |
| 初始化列表 | remove_const_t<Key>、T | 按传入的 Hash / Allocator 组合补齐 |
使用时的三条纪律:迭代器须满足标准库 InputIterator 要求;分配器须满足标准库 Allocator 要求;传入的 Hash / KeyEqual 不能被探测为 Allocator(且源码额外要求 Hash 非整型)。满足这些前提,即可放心依赖 deduction_guides.rst 描述的推导行为,并用 concurrent_unordered_map.h 中的实际签名核对推导结果。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考