移动迭代器:STL与移动语义的相遇

文摘   2024-11-14 09:21   广东  

点击上方【蓝字】关注博主

 移动迭代器是 C++11 中移动语义的扩展应用,它允许在 STL 中进行高效的元素移动操作,提高代码效率和性能。然而,在使用移动迭代器时需要注意数据丢失问题,并可以通过范围库等工具简化代码表达。

01

背景

C++11 的引入带来了许多新的语言特性和标准库扩展,其中一些特性协同工作以提供更强大、更灵活的编程方式。移动迭代器便是 STL 与移动语义协作的产物,它们允许开发者在简洁的代码中表达一系列重要的概念。

02

C++ 中移动语义的先决条件

在理解移动迭代器之前,我们需要了解移动语义的概念。移动语义是一种 C++11 中引入的优化技术,它允许在不进行复制的情况下将资源所有权从一个对象转移到另一个对象。

2.1、传统的复制机制

在移动语义出现之前,C++ 中唯一的对象实例化方式是通过复制:

class MyType {
public:
MyType(const MyType& otherObject) // 复制构造函数
{
// 执行将 otherObject 复制到此对象的代码
}
...
};

请注意:源对象(otherObject参数)是const。这可以是看成必须的,因为要进行复制,源对象只是用作模型,不需要被修改。

复制操作虽然可靠,并且被广泛使用,但在某些情况下会导致效率低下。例如,当一个对象包含指向动态分配内存的指针时,复制操作需要分配新的内存并复制数据,这会增加时间和空间开销。在这种情况下,进行复制并不是最好的解决方案。而且,如果可以通过修改源对象来更快地传输数据,那么利用它将非常有用。

事实证明,修改源对象有时可以加快数据传输速度。以 std::string 为例。它通常会将字符存储在一个动态分配的数组中。对于正在构造的字符串,获取源字符串数组的所有权比在复制过程中分配自己的数组要快得多。

2.2、移动语义的引入

为了解决复制操作带来的效率问题,C++11 引入了移动语义,它使用右值引用 (&&) 来表示可被 “移动” 的对象:

class MyType {
public:
MyType(MyType&& otherObject) // 移动构造函数
{
// 执行快速资源转移的代码,可能会修改 otherObject
}
...
};

移动构造函数接收右值引用作为参数,它允许快速、高效地将资源从源对象转移到目标对象,而不需要进行完整的复制操作。例如,对于包含动态分配内存的 std::string 对象,移动构造函数可以简单地将指向内存的指针转移到目标对象,而无需重新分配内存和复制数据。

2.3、使用 std::move 进行显式移动

开发者可以使用 std::move 函数将左值引用转换为右值引用,从而强制调用移动构造函数:

std::string s;
std::string sByCopy = s; // 调用复制构造函数
std::string sByMove = std::move(s); // 调用移动构造函数

std::move实际上并没有移动任何东西,而是通过将源对象强制转换为右值引用来将执行导向移动构造函数。

03

移动迭代器

移动迭代器是 STL 中的一项新功能,它允许在不进行复制的情况下将容器中的元素移动到另一个容器。

3.1、移动迭代器的用途

移动迭代器的主要目的是在进行容器操作时避免不必要的复制操作,提高效率。例如,当将一个容器中的元素复制到另一个容器时,使用移动迭代器可以将元素所有权直接转移,而无需进行复制。
实际上,STL 默认情况下是进行复制。示例:

std::vector<std::string> source = { "Move", "iterators", "in", "C++" };
std::vector<std::string> destination(begin(source), end(source));

输出它们将会是这样:

Source contains: "Move" "iterators" "in" "C++"
Destination contains: "Move" "iterators" "in" "C++"

destination 包含 source 元素的副本。使用移动迭代器将导致以下输出:

Source contains: "" "" "" ""
Destination contains: "Move" "iterators" "in" "C++"

其中每个字符串仍然存在于容器中,但其内容已被移动。但要注意,它与 std::move 不同:

std::vector<std::string> destination = std::move(source);

std::move 移动了整个vector

Source contains:
Destination contains: "Move" "iterators" "in" "C++"

3.2、如何使用移动迭代器

移动迭代器通过包装另一个迭代器来实现其功能。当被解引用时,移动迭代器会返回包装迭代器返回的右值引用。

当被解引用(使用 * 或 ->)时,STL 容器(如vector)迭代器返回指向它们指向元素的引用。解引用移动迭代器等效于在包装迭代器返回的引用上调用 std::move,以将其转换为右值引用。

例如,std::move_iterator 是一个类模板,其模板参数是它包装的迭代器的类型。为了简化使用,std::make_move_iterator 函数可以帮助我们进行类型推断:

std::vector<std::string> source = { "Move", "iterators", "in", "C++" };
std::vector<std::string> destination(std::make_move_iterator(begin(source)),
std::make_move_iterator(end(source)));

这段代码使用移动迭代器将 source 容器中的元素移动到 destination 容器中。每个元素仍然存在于 source 中,但其内容已被移走。输出如下:

Source: "" "" "" ""
Destination: "Move" "iterators" "in" "C++"

3.3、移动迭代器的注意事项

使用移动迭代器时,需要注意以下几点:

  • 移动迭代器会导致源容器中的元素失去其内容,因此在使用移动迭代器之后,应该避免再次使用源容器中的元素。

  • 如果移动迭代器被错误地使用,例如将元素移动到一个需要复制操作的函数中,会导致数据丢失。

  • 使表达一个简单事物所需的代码量激增。

04

避免数据丢失

使用移动迭代器时,需要特别小心,避免在操作过程中丢失数据。比如,如果 source 中的元素被移动到 destination 之外的某个地方,那么最后它们既不在 source 中也不在 destination 中,因此它们实际上就丢失了。

例如,在使用 std::copy_if 算法时,如果谓词参数接收的是按值传递的元素(而不是按 const 引用来传递元素),则元素在移动到谓词内部后会被销毁,导致数据丢失:

std::vector<std::string> source = { "Move", "iterators", "in", "C++" };
std::vector<std::string> destination;

std::copy_if(std::make_move_iterator(begin(source)),
std::make_move_iterator(end(source)),
std::back_inserter(destination),
[](std::string word){ return word.length() == 4; }); // 丢失数据!

std::copy_if 是一个 STL 算法,它遍历源集合并将满足谓词的元素复制到目标集合。但在这里使用移动迭代器,因此算法的输入变成了右值引用。输出将会是这样:

Source: "" "" "" ""
Destination: ""

所有数据都丢失了!这是因为谓词将元素移动进来,却没有将它们放回去。在使用移动迭代器时,要注意这类问题。

为了避免数据丢失,应该确保谓词参数接收的是元素的引用,或者使用其他方式将元素保留下来。

例如:

std::vector<std::string> source = { "Move", "iterators", "in", "C++" };
std::vector<std::string> destination;

std::copy_if(std::make_move_iterator(begin(source)),
std::make_move_iterator(end(source)),
std::back_inserter(destination),
[](std::string const& word){ return word.length() == 4; });

输出:

Source: "" "iterators" "in" "C++"
Destination: "Move"

在这里,移动迭代器上的 copy_if 已经转变为一种 move_if ,这有点道理。至少没有数据丢失。数据没有丢失的原因是,它并没有被移动到谓词内部:因为谓词接受引用,所以没有对象在谓词中被移动构造(或构造)。

代码示例:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <string>

int main() {
std::vector<std::string> source = { "Move", "iterators", "in", "C++" };
std::vector<std::string> destination;

// 使用移动迭代器将元素移动到 destination
std::copy(std::make_move_iterator(begin(source)),
std::make_move_iterator(end(source)),
std::back_inserter(destination));

// 打印结果
std::cout << "Source: ";
for (auto const& w : source) {
std::cout << '"' << w << '"' << ' ';
}
std::cout << "Destination: ";
for (auto const& w : destination) {
std::cout << '"' << w << '"' << ' ';
}
std::cout << std::endl;

return 0;
}

输出:

Source: "" "" "" "" 
Destination: "Move" "iterators" "in" "C++"


05

范围库的简化

使用移动迭代器虽然有效,但其代码表达方式相对繁琐,使用了大量代码来表达一个非常简单的事情,即使用元素的右值引用而不是元素本身。所以,需要一个简单的代码来表达它是必须的。范围库 (Ranges) 的引入简化了移动迭代器的使用。

在这里使代码冗长的原因是,它在抽象级别方面太低了。好的代码主要归结于尊重抽象级别。提高迭代器抽象级别的其中一种方法是将它们封装到一个范围内。

范围库提供了一个名为 view::move 的视图,它可以将任何范围转换为移动范围,完全实现了移动迭代器的目标,但可以用更简单的方式表达:

source | view::move; // 创建一个移动范围

这个移动范围可以用于各种算法,而无需显式使用移动迭代器(在查询时会移动源元素)。但要注意,它同样没有避免上面所示的错误情况下丢失数据。

参考 Eric Niebler’s Range Library(https://github.com/ericniebler/range-v3)。


06

总结

移动迭代器是 C++11 中移动语义的扩展应用,它允许在 STL 中进行高效的元素移动操作,提高代码效率和性能。然而,在使用移动迭代器时需要注意数据丢失问题,并可以通过范围库等工具简化代码表达。

公众号: Lion 莱恩呀

微信号: 关注获取

扫码关注 了解更多内容

点个 在看 你最好看


Lion 莱恩呀
专注分享高性能服务器后台开发技术知识,涵盖多个领域,包括C/C++、Linux、网络协议、设计模式、中间件、云原生、数据库、分布式架构等。目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。
 最新文章