这本书的全名叫《Effective STL — 50条有效使用STL的经验》,书其实已经有点历史了,Meyers在2001年时给它写的前言,如今已经2022了。 正如上面所说,因为这本书已经有些年头了,如果你已经是C++11、14、17、20的使用者,可以把第6章函数子、函数子类、函数及其他的内容快速带过看,因为std::functional以及lambda函数已经让C++函数类支持有了显著提升。 简略写一下对我而言有用的一些经验: 第12条:切勿对STL容器的线程安全性有不切实际的依赖 多线程读是安全的 多线程对不同容器做写入是安全的 第14条:使用reserve来避免不必要的重新分配 尽管我认为这应该是C++ Engineer的常识了,不过还是有好多小朋友不知道这个点。 第23条:考虑用排序的vector代替关联容器 可以详细看一下。我觉得根本问题是map类的容器对cache不是很友好。连续内存访问总是很香的。 第25条:熟悉非标准的散列容器 写书的时候还没有stl::unordered_map。不过即便放2022年,我也觉得可以看看STL之外的散列容器实现,比如robin-hood-hashing。曾经用过比较坑的一个容器是boost::flat_map,千万别认为它是O(1)的实现。 第31条:了解各种与排序有关的选择 <algorithm>头文件里有很多宝藏,在选择自己实现之前,最好看看STL有没有帮你写好了轮子。 第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase 曾经有个说法是std::move does not actually move anything, 对于std::remove也有点类似。 第44条:容器的成员函数优先于同名的算法 如题。 第47条:避免产生“直写型”(write-only)的代码 工程上非常中肯的一条经验。如果没有特殊原因(如及其苛刻的性能要求),尽量不要写出非常难懂的代码,不然几个月乃至几年之后你来调试自己写的代码时也会骂娘。 代码被阅读的次数远远大于它被编写的次数
Tag: C++
Boost Python的C++对象, Pickle支持及其原理
默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误 Pickling of "xxx" instances is not enabled. 这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html。 让你的class支持Pickle协议 若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite. 拿代码说话: struct world_t { world_t(const string& country) { … } }; struct world_pickle_suite : boost::python::pickle_suite { static boost::python::tuple getinitargs(const world_t& w) { // [可选实现] 返回一个boost::python::tuple元组,其值被用来构造 // 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。 return boost::python::make_tuple(w.country()); } static boost::python::tuple getstate(const world_t& w) { // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态, // 那么要用此函数返回其状态值 }… Continue reading Boost Python的C++对象, Pickle支持及其原理
Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)
为unix平台增加XZ多线程解压缩支持(基于7zip LZMA SDK, C/C++) Note This post has nothing to do with the pixz project. I am talking about decompressing the original xz archive using 7-zip’s LZMA SDK under unix environment. Background Originally the 7zip’s LZMA SDK (version 19.00) only covers parallel xz decompression for Windows systems. This post shows the C code that adds… Continue reading Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)
std::future 与 std::promise 简单使用、简单原理
C++11有了一些关于线程的模型,在此之前C++里可是各自为政的,各种线程库各种神奇用法。其中有两个好玩的东西就是std::promise<T>和std::future<T>,下文书中形容它们像是个“虫洞”。 std::future是虫洞的出口:一个未来对象的“获取器”,在未来的某一刻它能返回一个有用的值,但现在还没有… std::promise是虫洞的入口:我们要保证在未来的某一刻给一个值进去,但不是现在… Basic usage 一般来说这俩是成对使用的,看个代码: #include <cassert> #include <chrono> #include <future> #include <iostream> #include <string> #include <thread> using namespace std::chrono_literals; int main() { std::promise<int> p1, p2; std::future<int> f1 = p1.get_future(); std::future<int> f2 = p2.get_future(); p1.set_value(42); assert(f1.get() == 42); std::thread t([&]() { std::this_thread::sleep_for(100ms); p2.set_value(43); }); auto start_time = std::chrono::system_clock::now(); assert(f2.get() == 43); std::chrono::duration<double, std::milli> elapsed… Continue reading std::future 与 std::promise 简单使用、简单原理
C++17 std::variant 简单使用
std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。std::variant的类长这个样子: template <class… Types> class variant; Usage 使用起来也是比较方便的 std::variant<int, double> v1; v1 = 1; // activate the "int" member assert(v1.index() == 0); assert(std::get<0>(v1) == 1); v1 = 3.14; // activate the "double" member assert(v1.index() == 1); assert(std::get<1>(v1) == 3.14); assert(std::get<double>(v1) == 3.14); assert(std::holds_alternative<int>(v1) == false); assert(std::holds_alternative<double>(v1) == true); assert(std::get_if<int>(&v1) == nullptr); assert(*std::get_if<double>(&v1) == 3.14); 上面代码里的几个好用的constexpr函数:… Continue reading C++17 std::variant 简单使用
C++17 std::generate的使用
std::generate是“遍历——执行”工具函数的一种 #include <algorithm> #include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> v(4); std::generate(v.begin(), v.end(), [i=0]() mutable { return ++i % 2 ? "hello" : "world"; }); for (auto&& s : v) { std::cout << s << " "; } return 0; } 上面的函数输出是hello world hello world . std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。
C++17 新特性—— if constexpr
看代码说话 template <typename Iterator> auto distance(Iterator begin, Iterator end) { using Traits = std::iterator_traits<Iterator>; if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) { return end – begin; } else { auto result = typename Traits::difference_type(); for (auto it = begin; it != end; ++it) { ++result; } return result; } } 上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。在constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的… Continue reading C++17 新特性—— if constexpr
std::unique_ptr 关于智能指针的种种
这个域名也是突发奇想买到的,作为unique-ptr.com,为了对得起这个名儿,那就说一下智能指针算了。 前传。裸指针和auto_ptr 裸指针就是高度自治(要自己管)的指针,在符合基本法(误)的前提下,使用裸指针的人可以为所欲为。裸指针的一个问题是,它有点不符合现代C++内存管理的指导思想,使用者必须对它的生命周期负责,否则有可能发生: 内存泄漏(memory leak):如果new一大片内存但是由于代码复杂到头来忘了释放,就有可能耗尽内存资源;内存泄漏还有一种情况:如果申请内存做事情做了一半抛出了异常——代码跑到了exception的花括号里之后,原来申请的内存直接没办法拿到并且释放; 悬挂指针(dangling reference):指针已经被删除了,但其内存仍然在使用中;另外还有双重删除(double delete)的问题,当程序的某部分要删除一个已经被删除的指针时,即可出现这种情况;还有一种,指针所指的内存已经失效了(比如出了作用域了),但是后面的代码还用着指针; 应该是为了缓解这类问题,C++ 98标准搞出来了个智能指针:auto_ptr。相当于加了一个“管家”,在对象的内部保存那个裸指针,在管家析构的时候自动释放裸指针的内容,如果赋值的话那就把裸指针的所有权转交给新的管家。但是由于移动(move)语义是C++11才有的,auto_ptr在实现的时候,如果在两个智能指针对象之间作赋值(operator=)或者拷贝构造,它的参数都是非const类型的。为啥呢?因为它是会调用original_auto_ptr.release()方法拿到原始裸指针。这个有点无奈的操作是为了实现指针所指对象的“占有权”管理:两个auto_ptr不允许管理同一个目标裸指针。在“拷贝”的时候,相当于做了“主权移交”。 Copying an auto_ptr copies the pointer and transfers ownership to thedestination: both copy construction and copy assignment of auto_ptrmodify their right hand arguments, and the “copy” is not equal to theoriginal.— https://en.cppreference.com/w/cpp/memory/auto_ptr 这种操作并不符合常人对“拷贝“的理解,更像是”剪切“操作。且正是因为这个问题,切不可把auto_ptr放到stl容器里面,否则会得到各种编译错误。典型案例是这个:https://stackoverflow.com/questions/111478/why-is-it-wrong-to-use-stdauto-ptr-with-standard-containers另外由于析构的时候是用delete,auto_ptr里也不能放指针数组(那种需要用delete[]). unique_ptr 不同于auto_ptr,unique_ptr 的赋值运算符只接受典型地由 std::move 生成的右值。刚才说了,“右值”是C++11移动语义的产物,它比较好地解决了主权移交时的尴尬问题。这怎么说呢,一个智能指针的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程。回顾看之前说的裸指针的两个问题: 内存泄漏:由于析构函数里会delete管理的裸指针对象,所以在跑出对象作用域的时候,编译器会帮我们解决好问题,不用操心了; 悬挂指针:缓解,但不能完全避免。 一个例子是 #include <iostream> #include <memory>… Continue reading std::unique_ptr 关于智能指针的种种
记一次gtest EXPECT_DEATH遭遇sigsegv的debug过程
the crash 实习生小哥上周突然发现单元测试在dev机器上跑不起来了,–verbose一看发现是gtest报错Failure death test: 现象其实是所有写了EXPECT_DEATH的单元测试都挂掉了,感觉问题并不简单~ 问了一下边上的几位最近代码有什么变动,一说是用了个新的计算库。 gdb gdb一波,除看代码发现这个测试的原理是起一个子进程,然后把stderr写到一个临时文件里,子进程退出后,从临时文件里读取log信息,和预期值比对。发现第一现象是stderr没抓到,结果在找stderr为什么没抓到的邪路上走了很久,发现根本没产生stderr:/tmp下面写的文件,大小是0字节。 然后继续gdb,由于是要跟踪子进程,要把选项打开: set detach-on-fork off set follow-fork-mode child run了一下,看到了一点端倪:其实在fork之后进程就sigsegv了,而且居然是在动态加载glibc里的chdir函数过程中挂的。查了一下,机器上的glibc已经至少一年没有动过了,因此基本排除是glibc自己有问题。 然后开始各种翻来覆去的看,先看了几个local变量,发现一切都显得那么完美,根本没有任何内存值被搞烂的迹象。接着只能看挂掉的现场了,info registers看到寄存器的值如下: 突然发现红框处的rsp地址有点太“整”了,google了一下,确认这个地址应该是个用户态栈地址的下界。然后查看一下当前的指令,发现是一条push %ebx,也就是说还准备压栈————挂掉的清晰一些了,其实是栈地址空间不够了,也就是传说中的stack overflow。这也是为什么gtest能够看到程序挂掉,但是拿不到预期的错误信息的原因。 the stack overflow 那么,为什么栈空间不够了呢?翻了一下frame 0里的$rsp值,发现在0x7fc0附近,和刚才看到顶上的$rsp=0x7000相差了4032,也就是估计这个程序栈总的大小在4kb。查了一下ulimit -s有32767,诶? 于是接着看代码,发现gtest-death-test.cc里的做法, ExecDeathTestSpawnChild()里写道 mmap一个stack,它的大小是getpagesize() // ==4KB clone一个新的线程来跑测试例,用的这个新创建出来的stack 真相大白了。解决方法是mmap的时候分配多一点的栈空间。至于为什么到了上周才触发,大概是因为.so又多了几个吧~ 后记 当然,其实这个bug早就有fix了,只是我是后来通过搜”ExecDeathTestChildMain stack overflow”这个关键词才找到的。之前搜了半天EXPECT_DEATH没什么结果~相关的PR有: https://github.com/google/googletest/pull/1274 https://github.com/google/googletest/pull/2276 所以说thirdparty code还是要偶尔更新一下的呀~