C++11 版本已经有很长一段时间了, 它把 C++ 带入了一个新的境地, 引入了一些有用的特性, 这些特性使得编写并发代码变得容易, 需要掌握, 并且以今天这个时点, 2022年, 这些特性已经极其成熟, 并非什么新鲜事物了.
移动语义通俗讲, 就是将对象所持有的资源进行转移,
类比一下, 师傅有一把祖传宝剑, 名曰巨阙, 师傅收了个徒弟, 手上没有宝剑, 二人去龙潭屠龙, 到了地方, 师傅说我老了, 该退休了, 宝剑巨阙传给你, 去屠龙, 我去喝酒聊天钓鱼了. 宝剑巨阙就是资源, 师傅和徒弟是两个对象, 资源从师傅转移给徒弟, 就是移动语义, 之后徒弟有了资源可以去屠龙( 函数调用 ), 师傅退休( 等待析构, 或重新被转移资源 ).
有了移动语义, 我们就可以省了另铸一把巨阙剑的时间( 拷贝构造 ), 同时省了销毁原巨阙剑的时间( 析构 ), 里外里赚大发了.
而完美转发也很神奇, 它令传入的参数形式得以保留, 右值的还是右值, 左值的还是左值.
#include
// 移动语义
struct moveConstruct
{
moveConstruct()
: data(new int[10]())
{}
moveConstruct(const moveConstruct &rhs)
: data(new int[10])
{
std::copy(rhs.data, rhs.data + 10, data);
}
// 移动构造
// 通过移动语义转移资源
moveConstruct(moveConstruct &&rhs) noexcept
: data(rhs.data)
{
rhs.data = nullptr;
}
~moveConstruct()
{
delete[] data;
}
void show()
{
for (int i = 0; i != 10; ++i)
{
std::cout << *(data + i) << std::endl;
}
}
private:
int *data;
};
// 参数为右值引用的函数
void doStuff(moveConstruct &&rhs)
{
rhs.show();
}
template <typename T>
void print(T & /*unused*/)
{
std::cout << "left value" << std::endl;
}
template <typename T>
void print(T && /*unused*/)
{
std::cout << "right value" << std::endl;
}
// 完美转发
template <typename T>
void perfectForward(T &&rhs)
{
print(std::forward<T>(rhs));
}
auto main() -> int
{
// 右值引用
int &&rValueReference = 42;
std::cout << rValueReference << std::endl;
moveConstruct mCnstr;
// 移动构造时需要使用 std::move() 函数
moveConstruct mCnstr2(std::move(mCnstr));
// 函数中的右值引用
doStuff(std::move(mCnstr2));
// 完美转发, 右值引用解读为右值引用
// void perfectForward(int &&rhs)
perfectForward(42);
int test = 42;
// 完美转发, 左值引用解读为左值引用
// void perfectForward(int &rhs)
perfectForward(test);
return 0;
}
对于并发编程, 近乎所有的相关对象都是不可拷贝的, 大部分是可移动的, 例如 std::thread, std::unique_lock<>, std::future<>, std::promise<> 和 std::packaged_task<>, 如果不会使用移动语义, 则并发代码的限制将极大, 无法完成很多功能.
我们设计的很多类, 其语义是不支持拷贝的, 比如并发对象, unique_ptr 智能指针, 当我们设计一些封装这些并发对象的类或容器, 就必须禁止拷贝.
C++ 11 中引入删除函数, 可以轻松的做到这一点 ( 没有这个特性时, 是将拷贝构造和拷贝赋值放到私有函数中 )
// 只可移动不可拷贝的类
struct moveOnly
{
moveOnly()
: noCopy(new int[10]())
{}
// 不可拷贝
moveOnly(const moveOnly &) = delete;
// 不可拷贝赋值
auto operator=(const moveOnly &) -> moveOnly & = delete;
// 可移动构造
moveOnly(moveOnly &&rhs) noexcept
: noCopy(rhs.noCopy)
{
rhs.noCopy = nullptr;
}
// 可移动赋值
auto operator=(moveOnly &&rhs) noexcept -> moveOnly &
{
if (noCopy != rhs.noCopy)
{
delete[] noCopy;
noCopy = rhs.noCopy;
rhs.noCopy = nullptr;
}
return *this;
}
~moveOnly()
{
delete[] noCopy;
}
private:
int *noCopy = nullptr;
};
我们做线程池, 用到装载任务的队列, 装载线程的容器, 都是不可拷贝的, 必须将相应的构造函数, 赋值函数删除, 否则会给使用者很大的困惑.
对于 C++ 并发编程, lambda 在我看来是必须掌握的知识.
我们使用环境变量 std::condition_variable::wait( ), 需要它实现谓词, 使用 std::packaged_task< > 需要它包裹任务, 使用 std::thread 线程构造, 需要它提供可调用仿函数对象, 线程池中, 更需要它打包任务.
lambda 在 C++ 多线程编程中几乎无处不在, 有了它, 才能有简洁而清晰的代码.
所以, 必须掌握.
#include
#include
#include
#include
#include
#include
std::condition_variable condVar;
bool dataReady;
std::mutex mut;
void waitForData()
{
std::unique_lock<std::mutex> lock(mut);
condVar.wait(lock, [] { return dataReady; }); // 作为条件变量判断谓词
condVar.wait(lock, []() -> bool {
if (dataReady)
{
std::cout << "data ready" << std::endl;
return true;
}
std::cout << "data not ready, resuming wait" << std::endl;
return false;
});
}
auto makeOffseter(int offset) -> std::function<int(int)>
{
return [=](int j) { return offset + j; };
}
struct captureNumber
{
void foo(std::vector<int> &vec)
{
// 访问成员变量, 需要显示捕获 this
std::for_each(vec.begin(), vec.end(),
[this](int &i) -> void { i += numberData; });
}
private:
int numberData = 0;
};
// C++14 广义捕获
auto spawnAsyncTask() -> std::future<int>
{
// 局部 promise, 函数结束后销毁
std::promise<int> prms;
// 获取 future
auto ftr = prms.get_future();
// 开启线程, lambda 捕获移动对象, 将其转移至线程,
// 函数结束 prms 销毁, 也不会悬空引用
// 为了更改捕获对象, 需要加 mutable
std::thread thrd([prms = std::move(prms)]() mutable { prms.set_value(5); });
// 分离线程
thrd.detach();
// 返回 future
return ftr;
}
auto main() -> int
{
[]() { std::cout << 1 << std::endl; }(); // 直接调用
std::vector<int> iVec{1, 2, 3};
std::for_each(iVec.begin(), iVec.end(),
[](int i) { std::cout << i << std::endl; }); // 作为算法谓词
std::function<int(int)> const offset42 = makeOffseter(42);
std::function<int(int)> const offset123 = makeOffseter(123);
std::cout << offset42(12) << "," << offset123(12) << std::endl;
std::cout << offset42(12) << "," << offset123(12) << std::endl;
int offset = 42;
std::function<int(int)> offsetA = [&](int j) { return offset + j; };
offset = 52;
std::cout << offsetA(12) << std::endl; // 引用只会使用当前值 52, 输出64
int value = 5;
int refer = 10;
const std::function<int()> func =
[value, &refer]() { // 分别使用拷贝 value, 引用 refer
return value + refer;
};
value = 0;
refer = 20;
std::cout << func() << std::endl; // value 使用 5, refer 使用20
// C++ 14 后 lambda 可以使用 auto 推断参数, 变成通用 lambda
auto func2 = [](auto x) { std::cout << "x = " << x << std::endl; };
func2(42);
func2("hello");
return 0;
}
线程本地变量是每个线程独立的实例拷贝, 意味着同样的变量名称, 在每个线程可能拥有不同的值, 这在并发编程中, 不可或缺.
我们在讲防止死锁的层级锁, 风险指针构造的无锁栈, 中断线程都用到了线程本地变量.
对于全局和静态数据成员线程本地变量, 需要在线程使用前进行构建, 函数内部的非静态线程本地变量, 则只有在线程调用此函数时进行初始化.
目前用 lldb 和 gdb 难以直接追踪 thread_local 变量.
线程本地变量是可以被获取地址的, 如果其指针被传给其它线程, 和其它普通指针一样, 要注意其生存期, 不要超过其生存期进行指针解引用.
#include
#include
#include
// 命名空间内的线程本地变量
thread_local int thrdLcInt;
struct thrdLc
{
// 线程本地静态成员变量
static thread_local std::string thrdLcstr;
};
// 静态成员需类外定义
thread_local std::string thrdLc::thrdLcstr;
void foo()
{
// 函数内部线程本地变量
thread_local const std::vector<int> iVec{1, 2, 3};
for (const auto &i : iVec)
{
std::cout << i << std::endl;
}
}
auto main() -> int
{
foo();
thrdLc::thrdLcstr = "test";
std::cout << thrdLc::thrdLcstr << std::endl;
return 0;
}
今天先介绍到这里.