基础介绍
c++17引入了节点拼接特性,这个特性的引入旨在我们在不复制或者移动元素内容的情况下,将节点从一个容器转移到另一个容器。它的原理是通过转移底层数据结构来实现的,比如红黑树。通俗的理解就是将一个容器内部的元素转移到另外一个容器中,但是没有复制的开销。有点移动语义的意思,但与移动语义不同是,移动语义也会涉及内存的申请,在移动语义下即使没有复制整个数据,但是仍然需要申请一片内存来存储真实数据的指针等,而节点拼接则完全不需要申请内存,完全是通过数据结构操作实现的拼接,即两颗红黑树,将一个节点从一棵红黑树转移到另外一棵。所以节点拼接是针对容器而言的。而且节点拼接特性并不是所有的容器都支持,这里只有基于红黑树实现的map和set支持节点拼接特性。
传统方式vs节点拼接
// 传统方式 - 涉及复制/移动操作
std::map<int, std::string> map1 = {{1, "one"}, {2, "two"}};
std::map<int, std::string> map2;
// 方式1:复制 - 需要新分配内存
map2[1] = map1[1]; // 涉及复制构造
map1.erase(1); // 涉及内存释放
// 方式2:移动 - 仍需要处理原对象
map2[1] = std::move(map1[1]); // 涉及移动构造
map1.erase(1); // 涉及内存释放
//节点拼接方式
std::map<int, std::string> map1 = {{1, "one"}, {2, "two"}};
std::map<int, std::string> map2;
// 使用节点拼接 - 直接转移节点所有权
auto node = map1.extract(1); // 提取节点,不涉及复制/移动元素
map2.insert(std::move(node)); // 插入节点,只移动节点句柄
为什么节点拼接特性没有额外开销呢?
传统方式的元素转移,无论是移动语义(移动构造函数)还是直接复制(拷贝构造函数)都需要进行内存的操作,而节点拼接则不会,他完全是通过操作节点来实现的,没有任何的内存申请。朋友们可以想一想移动语义的实现,移动语义是通过移动构造函数或者移动赋值运算符实现,在移动语义下实现资源的转移,但是移动语义操作的对象本身是需要占用内存空间,移动语义转移的仅仅是对象的数据成员指针。所以从这个角度看节点拼接更彻底,但是局限性很大,因为其依赖于数据结构的实现(如红黑树),如果不是红黑树这样的数据结构,则一般无法实现节点拼接的特性。所以节点拼接仅仅针对map和set。请看下面的代码:
// 示例:大字符串的转移
std::map<int, std::string> source = {{1, "很长的字符串..."}};
std::map<int, std::string> target;
// 传统方式
// 1. 在target中分配新内存
// 2. 复制/移动字符串内容
// 3. 在source中释放原内存
target[1] = std::move(source[1]);
source.erase(1);
// 节点拼接方式
// 直接转移节点的所有权,不涉及字符串内容的处理
auto node = source.extract(1);
target.insert(std::move(node));
节点拼接的实现方法
- 通过extract方法:提取节点,需要注意的是extract方法返回的类型是std::map::node_type,一般使用auto推导即可。
- 通过insert方法:插入节点
- 通过merge方法:合并容器
请看下面的例子:
//insert与extract的运用
// 在map中更改键的类型而不重新分配值
std::map<int, std::string> map1 = {{1, "one"}, {2, "two"}};
std::map<std::string, std::string> map2;
auto node = map1.extract(1);
// 修改键的类型
node.key() = "1"; // 直接修改键
map2.insert(std::move(node));
//merge的应用
std::map<int, std::string> map1 = {{1, "one"}, {2, "two"}};
std::map<int, std::string> map2 = {{3, "three"}, {4, "four"}};
// 高效合并,直接转移节点
map1.merge(map2); // 不会产生任何元素的复制或移动
总结
- 避免了元素的复制/移动操作
- 避免了内存的重新分配
- 保持了容器的内部结构
- 提供了更灵活的容器操作方式