Tip of the Week 77 临时变量,移动,复制
我们正在尝试给语言专家意外的人解释清楚C++11会怎样改变代码世界,因此在“什么时候发生复制?”系列中又增加了一篇温江,这也是更广泛的尝试之中的一部分,试图用一套更简单的规则简化C++中的变量复制规则。
你会数到2吗?你会?太好了。记住“名字规则”意味着你能为一个资源指定的所有名字,会影响该对象有多少份拷贝。
简单解释数名字如果你在担心发生了变量复制,那我们驾驶你在担心几行特定的代码。在你认为被复制的数据上,有几个名字存在?只有三种情况需要考虑:
1. 两个名字:复制这个容易:如果你在给同一份数据第二个名字,那就是复制。
1234567std::vector<int> foo;FillAVectorOfIntsByOutputParameterSoNobodyThinksAboutCopies(&foo);std::vector<int> bar = foo; // 没错,这就是复制。std::map<int, string> my_map;string forty_two = "42";my_map[5] = f ...
Tip of the Week 24 复制简明判定法
“模仿别人是必要的,但重复自己是可悲的。” ——巴勃罗·毕加索
一个名字,没有复制;两个名字,两个副本在任何作用域内(包括触发RVO的情况),要知道发生了多少次复制,只需要检查你的数据有多少个名字。
在任意时刻,如果两份数据有两个有效的名字,那这些数据就有两个副本。作为一个很好的近似,
在除此以外的情况下,编译器会(而且尝尝是必须)避免复制。
因此,如果你的代码中,一份数据在某个时间点有两个名字,那么你应该期待有一次复制。如果你避免了指向一份数据的多余的名字,那么你在帮助编译器移除不必要的复制。
示例通过几个例子,我们来看看实践中是怎么使用这个定则的:
123456789101112131415std::string build();std::string foo(std::string arg) { return arg; // 没有复制,数据`arg`只有一个名字。}void bar() { std::string local = build(); // 只有一个实例,只有一个名字 // 没有复制,引用不会触发复制 std::string& ...
Tip of the Week 11 返回策略
注:这条贴士—虽然仍然有效—但是写它的时候还没有C++11的移动语义。同时可以参阅Totw #77。
很多老的C__代码库使用了“害怕复制对象”的范式。幸运的是,多亏了“返回值优化(RVO)”,我们可以“复制”但不真的复制。
RVO特性存在已久,几乎所有的C++编译器都实现了它。考虑如下的C++98代码,有一个复制构造函数和一个复制运算符。这些函数代价都很大,开发者让它们在每次调用时都打印一条消息:
123456789101112131415class SomeBigObject { public: SomeBigObject() { ... } SomeBigObject(const SomeBigObject& s) { printf("死贵的复制…\n", …); … } SomeBigObject& operator=(const SomeBigObject& s) { printf("死贵的赋值…\n", …); … ...
Tip of the Week 10 分割字符串,不必拘小节
将字符串分割为子串是任何通用编程语言的常见操作,C++也不例外。这个需求出现在Google的时候,很多工程师发现他(她)们掉进了一堆(且不断增加的)“分割函数”填成的泥沼。你必须搜索那个输入参数、输出参数和语义都满足你要求的函数。在考察过600多行的头文件理的里的50多个函数之后,你也许会最终决定使用一个命名拐弯抹角的函数:
SplitStringViewToDequeueIfStringAllowEmpty()。
为了解决这个麻烦,C++库团队实现了一个新的API来分割字符串,放在了absl/strings/str_split.h里。
这个新的API用一个absl::StrSplit()函数取代了所有类似的分割函数。absl::StrSplit()接受一个准备被分割的字符串和一个分割符参数。其返回的子串集合可以自动适配为调用者接受的容器类型。absl::StrSplit()的内部实现使用absl::string_view,因此非常高效。除非调用者显式要求将结果存储为字符串对象集合,否则字符串内容不会被复制。
来看下下面的例子,比较直截了当:
123456789101112131415 ...
Tip of the Week 5 消逝的演出
有时候,为了正确的运用C++的库,你既需要理解库本身,又需要理解这门语言。那么……下面的代码中的问题是什么?
12345// 别这么干std::string s1, s2;...const char* p1 = (s1 + s2).c_str(); // 别!const char* p2 = absl::StrCat(s1, s2).c_str(); // 别!
s1+s2和absl::StrCat(s1, s2)都创建了临时对象(这里都是字符串对象,但同样的规则使用于任意对象)。成员函数c_str()返回指向底层数据的指针,而底层数据与临时对象生命周期一致。临时对象能活多长?根据C++17标准中的[class temporary],“在临时对象创建点所在的完整表达式中,临时变量的销毁时表达式的最后一步。”,那么也就是说,当赋值运算符右边的表达式结束的时候,临时变量就被销毁了,c_str()的返回值就成了“野”指针。那么如何避免这类问题呢?
方法一,在完整表达式结束前用完临时对象:123// 安全(虽然弱鸡了一点)size_t len1 = strlen(( ...
Tip of the Week 3 字符串拼接,operator+ vs StrCat()
当审查代码的人对程序员说“别用字符串拼接操作,太低效了”,程序员尝尝会一脸吃惊:std::string::operator+怎么会低效呢?难道这不是很难出错的方式吗?
事实证明,这种低效没有那么简单明了。在实践中,下面两段代码执行时间相差不多:
123456std::string foo = LongString1();std::string bar = LongString2();std::string foobar = foo + bar;std::string foo = LongString1();std::string bar = LongString2();std::string foobar = absl::StrCat(foo, bar);
然而,同样的结论却不适用于下面两段代码:
123456789std::string foo = LongString1();std::string bar = LongString2();std::string baz = LongString3();string foobar = foo + bar + baz;std::str ...
C++线程同步之互斥锁
进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的认为干涉就会出现各种各样的错误数据。这时因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满,现实很残酷。
解决多线程数据混乱的方案就行进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:
**std::mutex**: 独占的互斥锁,不能递归使用
std::timed_mutex: 带超时的独占互斥锁,不能递归使用
**std::recursive_mutex**: 递归互斥锁,不带超时功能
**std::recursive_timed_mutex**: 带超时的递归互斥锁
互斥锁在有些资料中也被称之为互斥量,两者是一个东西。
1. std::mutex不论是在C还是C++中,进行线程同步的处理流程基本上是一致的,C++中的 ...
call_once
在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候,需要一个once_flag()作为call_once()的传入参数,该函数的原型如下:
123// 定义于头文件 <mutex>template< class Callable, class... Args >void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
flag: once_flag类型的对象,要保证这个对象能够被多个线程同事访问到
f: 回调函数,可以传递一个有名函数地址,也可以指定一个匿名函数
arg: 作为实参传递给回调函数
多线程操作过程中,std::call_once()内部的回调函数只会被执行一次,示例代码如下:
1234567891011121314151617181920212223242 ...
Tip of the Week 1 string_view
参考链接:abseil.io/tips/1
什么是string_view,为什么需要string_view当你在把一个字符串(或者字符串常量)作为一个函数参数的时候,通常有三种选择:前面两种你肯定是知道的,剩下的你可能没有意识到:
123456789// C Conventionvoid TakesCharStar(const char* s);// Old Standard C++ conventionvoid TakesString(const std::string& s);// string_view C++ conventionsvoid TakesStringView(absl::string_view s); // Abseilvoid TakesStringView(std::string_view s); // C++17
当调用者传入字符串格式的参数来调用函数,前面两种方式是可以正常工作的,但是在这个过程中是如何做转化的呢?无论是从const char* 到 std::string还是从std::string到const char*?
调用者 ...
命名空间-this_thread
在C++11中不仅添加了线程类,还添加了一个关于线程的命名空间std::this_thread,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。
1. get_id调用命名空间std::this_thread中的get_id()方法可以得到当前线程的线程ID,函数原型如下:
1thread::id get_id() noexcept;
关于函数使用对应的示例的代码如下:
123456789101112131415#include <iostream>#include <thread>using namespace std;void func(){ cout << "子线程: " << this_thread::get_id() << endl;}int main(){ cout << "主线程: " << this_thread::get_id() << endl; ...