Administrator
发布于 2023-10-28 / 5 阅读 / 0 评论 / 0 点赞

10月28日

链接

【公开课】现代C++进阶:模板元编程与函数式_哔哩哔哩_bilibili

目录

模板和重载的结合

特殊情况

const char* 的他会被用T,而不是string重载

返回值T

整数作为模板

注意,这里只支持整型int

提问:这种写法,和直接让int 作为参数传入有什么区别?

time<3> ("one"):

这里会直接在编译器编译时优化,int可能的值都会生成一个函数。

N时编译时常数

time (3,"one");

这就是正常函数

N是运行时常数

参数特化

后果:

会导致编译时间过长,且模板内部实现必须暴露出来,负责,定义和实现必须放在头文件里

编译期优化案例

此时,debug是运行时判断

用模板优化后

if constexpr 编译期分支

如果debug这个变量不是编译器常量int类型,类似于c的#ifdef debug

编译器常量的限制

解决办法

如果调用表达式,该表达式也必须constexpr,并且声明和定义不能分离

std::min也是constexpr函数,可以直接用

如何让模板的定义和声明分离

由于模板是惰性的,你用到什么他才会在编译时生成什么,所以,如果一定需要定义和声明分离,你必须声明编译器常量的所有情况

延迟编译

没人用的模板,甚至错误都不管

实现打印vector

inline是上个时代的产物,现在编译器默认帮你内联

引用

语法糖,就是指针,并且是必须被初始化的指针,能帮你解引用

const &

编译器敢帮你大胆的优化

函数返回引用

static ,只有进入该函数才会被实例化

右值

const int p和int const p没有区别

const int* p和int *const p,不一样

decltype

decltype(a),获取a的类型(以及是左值和右值)

万能推导

decltype(auto)=func(),甚至连,可变性,左右值引用都推导出来,

单单auto,是需要自己写const,&,&&的。

using
实现2个不同类型的vector相加

部分简写

目录

函数是编程

模板化

lamba表达式

返回值

避免拷贝开销

返回lamda表达式

[&] 的错误

此时 fac已经出作用域,fac被释放了,而【&】中保存的是引用,自然是无效的,此时应该用【=】或者move fac。

【】这种无捕获的lamda可以传为函数指针

lamda+模板

yield模式

匿名递归

tuple

optional
value_or 指定缺省值
*optional 不检测为空,不抛异常

variant:安全的union,存储多个不同类型

std::visit批量匹配

c++多线程

大纲

时间

c语言

c++ std::chrono

double显示时间

跨平台的sleep()
睡到某个时间

进程和线程

进程

一个程序的一次执行,拥有隔离的内存空间。多个进程之间交流困难,需要通过文件或者socket

线程

系统的独立分配和调度的基本单位,进程本身不能获得cpu时间,只有线程可以。

std::thread

错误:找不到

.join()等待子进程结束。

分离线程

让子线程

把join()函数写在析构函数里

jthread 自动join

异步

std::async

get() 等待执行结束并返回结果

wait() 仅仅是等待执行结束。

wait_for()只等一段时间

不调用线程的异步std::launch::deferred
std::promise

set_value()和get_future()

std::shared_futrure<void> fret=std::async([&]{})

future因为三五法则,删除拷贝/赋值函数,如果需要浅拷贝用std::shared_futrure

vector不是线程安全的

多个线程访问一个vectoer会出现数据竞争

std::mutex

lock_guard自动上锁和解锁

出作用域自动解锁

unique_lock可以提前解锁

std::defer_lock作为参数

默认unique_lock,构造时有上锁,而std::defer_lock作为参数时,创造unique_lock对象,但不先不上锁。

使用场景:

避免死锁,当你需要对多个互斥量进行加锁,并希望避免死锁的发生。

先对多个互斥量都创建对象,然后一起上锁,保证都上锁或者都不上锁。避免锁了一些又没锁一些。最终在多线程中造成死锁

try_lock()不等待的上锁

空闲就锁,不空闲就直接返回。

try_lock_for() 等一段时间

创建unique_lock时,以try_to_lock为参数

用owns_lock()判断成功失败

std::adopt_lock参数

通常用于在多线程编程中传递已锁定的互斥量给函数或存储在数据结构中。并确保互斥量的锁定状态是有效和一致的。

一点补充:鸭子类型

死锁难题

解决方法1:

不要同时持有2把锁

解决方法2:

上锁的顺序要相同,不要颠倒顺序

解决方法3:

保证对多个锁同时上锁,std::lock(mtx1,mtx2,...);

std::scoped_lock

RAII版本的std::lock。

std::lock 不同,std::scoped_lock 在构造时就会对互斥量进行锁定,而不是在调用 std::lock 函数时进行锁定。这样可以避免手动调用 std::lock,使代码更简洁和易读。

std::scoped_lock 可以帮助你避免死锁的发生。它使用了一种叫做 "资源获取即初始化(Resource Acquisition Is Initialization, RAII)" 的技术,确保在构造 std::scoped_lock 对象时,互斥量会按照特定的顺序进行锁定,从而避免死锁的可能性。

你提出的观点是正确的,`std::lock` 和 std::scoped_lock 在功能上有一些重叠,它们都可以用来同时锁定多个互斥量,并在正确的顺序上锁。这可能会导致你感到困惑,为什么存在两个功能如此相似的函数。

实际上,`std::scoped_lock` 是在 C++17 中引入的,而 std::lock 则是在更早的 C++11 中引入的。当引入 std::lock 时,C++标准库还没有提供一种直接的方式来同时锁定多个互斥量,因此 std::lock 函数填补了这个空缺。它的设计目的是让程序员能够安全地锁定多个互斥量,避免死锁的发生。

随着 C++17 的到来,引入了 std::scoped_lock,它提供了一种更简洁和直观的方式来锁定多个互斥量。`std::scoped_lock` 使用了 RAII 技术,能够自动释放锁,并且在构造函数中指定要锁定的互斥量。这使得代码更加易读、易于维护,并提供了额外的安全性保证。

尽管 std::lockstd::scoped_lock 在功能上有一些重叠,但它们仍然存在一些区别和用例上的差异。在实际使用中,你可以根据个人或团队的偏好来选择使用哪个函数。

总结一下,`std::lock` 和 std::scoped_lock 在功能上有一定的重叠,都可以用来同时锁定多个互斥量。它们的存在是为了满足不同的编程需求和个人偏好。`std::scoped_lock` 是在 C++17 中引入的,提供了更简洁、易读和安全的编程体验,而 std::lock 则是在早期的 C++11 中引入的,填补了同时锁定多个互斥量的空缺。

同一线程的死锁现象

用std::recursive_mutex解决

如果是同一个线程多次上锁,会计数器+1而不是一直等待上锁,毕竟同一线程的2个操作不可能出现数据竞争。

封装一个线程安全的vecter

bug:原来的size_t size() const{}

但是我们新加的m_mtx.lock()不是const,与旧版本不兼容

解决方法:mutable用修饰 std::mutex,使其可变。

mutable

在 C++ 中,`mutable` 是一个关键字,用于修饰类的成员变量。当一个成员变量被声明为 mutable 时,它可以在被声明为 const 的成员函数中被修改,即使该函数不会修改对象的其他状态。

mutable 的作用是放宽了 const 修饰符对成员变量的限制,允许在 const 成员函数中修改特定的成员变量。这对于需要在逻辑上保持对象不变,但某些状态仍然需要可以修改的情况非常有用。

以下是一些需要使用 mutable 的常见情况:

1. 缓存结果:在某些情况下,你可能希望在对象的某个成员函数第一次被调用时计算并缓存结果,在后续的调用中直接返回缓存的结果,而无需重新计算。你可以使用一个 mutable 成员变量来存储缓存的结果,并在计算时进行更新,即使该成员函数被声明为 const

```cpp

class Cache {

mutable bool m_isCached;

mutable int m_cachedResult;

public:

int getResult() const {

if (!m_isCached) {

m_cachedResult = calculateResult(); // 修改 mutable 成员变量

m_isCached = true;

}

return m_cachedResult;

}

int calculateResult() const {

// 计算结果的逻辑

}

};

```

2. 计数器或状态跟踪:在某些情况下,你可能需要在对象的 const 成员函数中跟踪某个状态或计数。例如,在一个线程安全的类中,你可能希望在 const 成员函数中计算某个操作的调用次数,你可以使用 mutable 成员变量来存储和更新这个计数。

```cpp

class Counter {

mutable int m_count;

public:

Counter() : m_count(0) {}

void increment() const {

++m_count; // 修改 mutable 成员变量

}

int getCount() const {

return m_count;

}

};

```

需要注意的是,虽然 mutable 允许在 const 成员函数中修改成员变量,但它并不意味着你可以在任何情况下都随意修改。你仍然需要遵循适当的设计原则和约定,确保 mutable 的使用合理且符合逻辑。

总结一下,`mutable` 关键字允许在被声明为 const 的成员函数中修改特定的成员变量。它在需要在逻辑上保持对象不变,但某些状态仍然需要修改的情况下非常有用,例如缓存结果或跟踪状态。然而,使用 mutable 时应当谨慎,确保其使用合理且符合设计原则。

const 修饰成员函数的意义

在 C++ 中,将成员函数声明为 const 的目的是为了表示该函数不会修改对象的状态。这是一种约定,它告诉编译器和其他开发者,调用该函数不会导致对象的状态发生变化。

声明成员函数为 const 有以下几个方面的意义和好处:

1. 安全性:`const` 成员函数可以提供一定程度的安全性保障。当一个对象被声明为 const 时,它的 const 成员函数可以在对象的任何状态下进行调用,而不会引发意外的修改。这对于使用该对象的代码来说是一种保证。

2. 可读性和理解性:使用 const 来修饰成员函数可以更清晰地表达函数的意图。当一个函数被声明为 const 时,它的命名和函数体内的代码应该表明它只是读取对象的状态,而不会修改它。

3. 逻辑常量性:通过将成员函数声明为 const,你可以将逻辑常量性(logical constness)应用于对象。逻辑常量性是指在对象的内部,即使没有使用 mutable 关键字修饰的成员变量,也可以保持不变。这使得你可以对 const 对象调用相应的 const 成员函数,而无需担心对象的状态被修改。

当你有以下需求时,你可能会考虑将成员函数声明为 const

- 读取对象的状态而不修改它。

- 避免在 const 对象上调用非 const 成员函数时产生编译错误。

- 允许 const 对象调用适当的成员函数,以便进行状态的检查或查询。

- 提高代码的可读性和可维护性,让其他开发者更容易理解你的代码的意图。

需要注意的是,如果成员函数需要修改对象的状态,或者调用了其他非 const 成员函数,那么它不能声明为 const。在这种情况下,你应该省略 const 修饰符,以允许函数修改对象。

总结一下,将成员函数声明为 const 是一种约定,用于表示该函数不会修改对象的状态。这样做可以提供安全性、可读性和逻辑常量性,并且在某些情况下可以提高代码的可维护性和可理解性。

读写锁

shared_mutex

读:lock_shared()、unlock_shared()

写:lock()、unlock()

lock_shared()

访问者模式Accessor模式

条件变量std::condition_variable

cv.wait()

cv.wair(lck,expr) 等待某一条件为真

提问:

condition_variable是怎么实现只通知一个和通知所有?std::unique_lock lck(mtx);这个语句在cv.wait(lck);的前面,那理论上第一个锁上了后,其他的线程根本执行不到cv.wait(lck);这个命令,为什么, cv.notify_all()后,大家都执行了?

答案:

通过使用 std::condition_variablestd::mutexstd::unique_lock 的组合,可以实现线程的等待和通知机制。当某个线程调用 wait() 时,它会释放互斥量并进入等待状态,直到收到通知并重新获得互斥量的锁。而 notify_one() 会唤醒其中一个等待的线程,而 notify_all() 会唤醒所有等待的线程。这样可以实现线程之间的同步和协调。

生产者和消费者模式

其他补充

原子操作

std::atomic<int>
原子操作下的+=和+ =
fetch_add和+=(取然后加)
exchange读取并且写入

会返回旧值

compare_exchange_strong(old,val)

bool a=counter.compare_exchange_strong(old,val);

counter与old相等 ,counter=val;

counter与old不相等 ,old=counter;

方便理解的伪代码

讨论const修饰函数,保护了什么?

void push_back(int val)const {
            return m_that.arr.push_back(val);
        }
 push_back为啥可以是const的?

抱歉,我之前的回答可能引起了误解。让我更正一下。

在代码中,`push_back`函数调用了`m_that.arr.push_back(val)`,它向`m_that.arr`容器中添加了一个元素`val`。这实际上是修改了容器的状态,因为容器的大小增加了一个元素。

当我说`push_back`函数不会修改对象的状态时,是指`push_back`函数本身并不会修改`m_that`对象的其他成员变量。但是,由于`push_back`函数操作了`m_that.arr`容器,它会修改容器的状态,即向容器中添加新元素。

因此,从容器的角度来看,`push_back`操作是修改了容器的值。但是,从`push_back`函数所在的类对象的角度来看,它不会修改除了`m_that.arr`以外的其他成员变量。

总结一下,`push_back`函数会修改容器的状态,但不会修改调用该函数的类对象的其他成员变量。

git 拉去子模块

根据您提供的命令输出,看起来在您的存储库中已经存在名为 "hw05" 的条目,因此无法再次添加相同的子模块。

如果您确定要重新添加子模块并覆盖现有的条目,您可以按照以下步骤进行:

1. 删除现有的子模块条目。运行以下命令:

```shell

git submodule deinit -f hw05

git rm -f hw05

```

2. 确认已删除子模块的条目。运行以下命令:

```shell

git status

```

确保不再显示 "hw05" 的条目。

3. 执行添加子模块的命令。运行以下命令:

```shell

git submodule add <URL>

```

<URL> 替换为正确的子模块 URL。

这将重新添加子模块到您的存储库中。

请注意,这些操作可能会导致您丢失与现有子模块相关的更改。请确保在执行这些命令之前备份或提交您的更改。


评论