1.std::move
是 C++11 中引入的一个函数模板,用于将一个左值转换为右值引用。它的定义如下:
template<typename T>
typename remove_reference<T>::type&& move(T&& arg) noexcept;
其中,T&& 表示右值引用,noexcept 表示该函数不会抛出异常。std::move 的作用是将一个左值转换为右值引用,从而实现移动语义。移动语义是指,将一个对象的资源所有权从一个对象转移到另一个对象,而不是进行复制操作。移动语义可以避免复制大量数据的开销,提高程序的效率。
使用 std::move 的示例代码如下:
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> v1 {"hello", "world"};
std::vector<std::string> v2 = std::move(v1);
std::cout << "v1.size() = " << v1.size() << std::endl;
std::cout << "v2.size() = " << v2.size() << std::endl;
return 0;
}
//输出结果应该为:
//v1.size() = 0
//v2.size() = 2
在上面的代码中,我们定义了两个 std::vector < std :: string> 类型的对象 v1 和 v2,并使用 std::move 函数将 v1 的所有权转移给 v2。由于 std::vector 类型的对象是动态分配的,转移所有权相当于将 v1 中的内存地址和数据交给了 v2,而不是进行复制操作。因此,当 v1 的所有权被转移后,v1 中的数据已经不存在了,v1.size() 的结果应该为 0,而 v2 中的数据和 v1 中的数据一样,v2.size() 的结果应该为 2。
可以看到,使用 std::move 可以实现移动语义,避免了复制大量数据的开销,提高了程序的效率。需要注意的是,使用 std::move 时需要确保对象的状态是合法的,并且在转移所有权后,原来的对象不应该再访问该资源。
2.std::function
std::function 是一个通用的函数封装类模板,可以用于存储、复制和调用任意可调用对象(函数、函数指针、仿函数、Lambda表达式等)。
std::function 的定义方式如下:
template<class R, class... Args>
class function<R(Args...)>;
其中,R 表示返回值类型,Args… 表示参数类型列表。
std::function 的主要功能是将可调用对象封装成一个函数对象,并提供函数调用运算符 (),使得可以像调用普通函数一样调用这个函数对象。std::function 还可以进行赋值、拷贝、比较和空值检查等操作。
以下是一个使用 std::function 的示例代码:
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
class MyFunctor {
public:
int operator()(int a, int b) {
return a - b;
}
};
int main() {
// 调用函数
std::function<int(int, int)> f1 = add;
std::cout << f1(1, 2) << std::endl;
// 调用函数指针
int (*p)(int, int) = add;
std::function<int(int, int)> f2 = p;
std::cout << f2(3, 4) << std::endl;
// 调用仿函数
MyFunctor f3;
std::function<int(int, int)> f4 = f3;
std::cout << f4(5, 6) << std::endl;
// 调用Lambda表达式
std::function<int(int, int)> f5 = [](int a, int b) { return a * b; };
std::cout << f5(7, 8) << std::endl;
return 0;
}
在上面的代码中,我们定义了一个名为 add 的函数、一个名为 MyFunctor 的仿函数、以及一个Lambda表达式,用于实现加法、减法和乘法运算。然后,我们使用 std::function 分别创建了 f1、f2、f4 和 f5 四个函数对象,并进行了函数调用。其中,f1 调用了函数 add,f2 调用了函数指针 p,f4 调用了仿函数 f3,f5 调用了Lambda表达式。最后,我们输出了函数调用的结果。
需要注意的是,使用 std::function 调用对象时,需要保证对象的函数签名(返回值类型和参数类型)与 std::function 的模板参数一致。
3.std::once_flag
std::call_once
std::once_flag 是 C++11 中的一个标准库类,用于保证某个函数只被执行一次。它通常与 std::call_once 函数一起使用,用于实现线程安全的单例模式。
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []() { instance = new Singleton(); });
return *instance;
}
void print() {
std::cout << "Hello, Singleton!" << std::endl;
}
private:
Singleton() {}
static Singleton* instance;
static std::once_flag initFlag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
std::cout << std::boolalpha << (&s1 == &s2) << std::endl; // 输出 true
s1.print(); // 输出 "Hello, Singleton!"
return 0;
}
在上面的代码中,我们定义了一个单例类 Singleton,并使用 std::once_flag 和 std::call_once 函数来保证该类只被初始化一次。在 getInstance() 函数中,我们使用 std::call_once 函数来确保 init() 函数只被执行一次,并在其中初始化单例对象。在 init() 函数中,我们使用 new 运算符来分配内存,并在静态成员变量 instance 中保存单例对象的指针。
需要注意的是,std::once_flag 和 std::call_once 可以保证线程安全的单例模式,但并不是最高效的实现方式。在多线程环境下,静态变量的初始化可能会导致锁的竞争和延迟,从而降低程序的效率和性能。因此,在实际编程中,需要根据具体情况选择合适的单例模式实现方式。
4.std::condition_variable
下面是一个使用 std::condition_variable 实现生产者-消费者模型的例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;
void producer() {
for (int i = 1; i <= 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
q.push(i); // 生产数据
cv.notify_one(); // 通知消费者线程
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
while (q.empty()) { // 等待队列非空
cv.wait(lock);
}
int data = q.front(); // 消费数据
q.pop();
std::cout << "Consumer: " << data << std::endl;
}
}
int main() {
std::thread t1(producer); // 创建生产者线程
std::thread t2(consumer); // 创建消费者线程
t1.join();
t2.join();
return 0;
}
在上面的代码中,我们定义了一个互斥锁 mtx 和一个条件变量 cv,并使用一个队列 q 来共享数据。在生产者线程中,我们使用 q.push(i) 生产数据,并使用 cv.notify_one() 通知消费者线程。在消费者线程中,我们使用 cv.wait(lock) 等待队列非空,并在队列非空时消费数据。在消费数据后,我们使用 q.pop() 弹出数据,并输出消费的结果。
需要注意的是,在使用条件变量时,需要在互斥锁的保护下进行等待和通知操作。在等待条件时,需要使用 cv.wait(lock) 将线程阻塞,并释放互斥锁,以允许其他线程访问共享资源。在通知等待线程时,需要使用 cv.notify_one() 或 cv.notify_all() 通知等待线程,并让其重新竞争互斥锁。在使用条件变量时,需要特别小心死锁和资源竞争等问题,需要采取适当的同步措施,例如使用互斥锁和条件变量等,以保证线程间的安全和正确性。
5. std::lock_guard
std::unique_lock
std::lock_guard 和 std::unique_lock 都是 C++ 标准库中用于管理互斥锁的 RAII(Resource Acquisition Is Initialization)类,用于在获取互斥锁时自动加锁,并在对象生命周期结束时自动解锁互斥锁。
两者的主要区别在于灵活性和可扩展性。具体来说:
std::lock_guard 是一个轻量级的 RAII 类,无法手动释放互斥锁,也无法进行延迟加锁和解锁等操作。因此,std::lock_guard 更适合简单的互斥锁管理场景,例如在函数内部获取互斥锁,并在函数返回时自动释放互斥锁。
std::unique_lock 是一个灵活的 RAII 类,可以手动释放互斥锁,并支持延迟加锁和解锁等操作。因此,std::unique_lock 更适合复杂的互斥锁管理场景,例如需要在函数内部多次获取和释放互斥锁,或需要支持条件变量等高级特性。
以下是两者的使用示例:
#include <mutex>
// 使用 std::lock_guard 管理互斥锁
void foo() {
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx); // 获取互斥锁
// 临界区代码
} // 自动释放互斥锁
}
// 使用 std::unique_lock 管理互斥锁
void bar() {
std::mutex mtx;
{
std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁
// 临界区代码
lock.unlock(); // 手动释放互斥锁
// 非临界区代码
lock.lock(); // 手动获取互斥锁
// 临界区代码
} // 自动释放互斥锁
}
需要注意的是,在使用 RAII 类管理互斥锁时,必须避免手动释放互斥锁和多次获取和释放互斥锁等问题,以避免出现竞态条件和数据不一致等问题。同时,在使用条件变量等高级特性时,需要使用 std::unique_lock 类来管理互斥锁,以便于支持延迟加锁和解锁等操作。
6.std::semaphore
信号量(Semaphore)是一种用于线程间同步和互斥的机制,它可以用来保护共享资源,避免出现竞态条件和数据不一致等问题。信号量包括计数型信号量和二进制信号量两种类型,其中计数型信号量的计数值可以大于 1,而二进制信号量的计数值只能是 0 或 1。
在 C++ 标准库中,可以使用 std::semaphore 类来实现信号量,该类定义在 头文件中,是 C++20 新增的标准库特性。std::semaphore 类提供了 wait() 和 post() 两个成员函数,分别用于等待信号量和释放信号量。例如,以下代码实现了一个计数型信号量:
#include <semaphore>
#include <iostream>
std::semaphore sem(0); // 创建计数型信号量,计数值为 0
void producer() {
for (int i = 1; i <= 10; ++i) {
// 生产数据
std::cout << "Producing data: " << i << std::endl;
// 释放信号量
sem.post();
}
}
void consumer() {
for (int i = 1; i <= 10; ++i) {
// 等待信号量
sem.wait();
// 消费数据
std::cout << "Consuming data: " << i << std::endl;
}
}
int main() {
std::thread t1(producer); // 创建生产者线程
std::thread t2(consumer); // 创建消费者线程
t1.join();
t2.join();
return 0;
}
在上面的代码中,我们创建了一个计数型信号量 sem,初始计数值为 0。在生产者线程中,每次生产完数据后,调用 sem.post() 函数释放一个信号量,从而通知消费者线程可以消费数据了。在消费者线程中,调用 sem.wait() 函数等待一个信号量,从而等待生产者线程生产新的数据。当有新的数据可用时,生产者线程会释放一个信号量,消费者线程会获取一个信号量,并消费相应的数据。通过信号量的等待和释放,我们可以实现线程间的同步和互斥,保证程序的正确性和安全性。
需要注意的是,在使用 std::semaphore 类时,需要使用 #include 头文件,并链接 -pthread 选项以启用 POSIX 线程库。同时,需要注意正确的信号量计数值和信号量的等待和释放操作,以避免出现死锁和竞态条件等问题。