Effective Modern C++ 阅览笔记(最新颖的名词是‘通用引用’和‘完美转发’,C++语言真是博大精深,哈哈)
Effective Modern C++ 阅读笔记(最新颖的名词是‘通用引用’和‘完美转发’,C++语言真是博大精深,哈哈)
Effective Modern C++
目录
- 1 Deducing Types
- 2 auto
- 3 Moving to Modern C++
- 4 Smart Pointers
- 5 Rvalue References, Move Semantics, and Perfect Forwarding
- 6 Lambda Expressions
- 7 The Concurrency API
- 8 Tweaks
Deducing Types
- 理解模板类型参数推导
- reference-ness忽略
- universal reference:T&&
- 数组类型:T (&)[N]
- 推导by-value参数时,const和volatile被忽略
- 理解auto类型推导
- auto x3 = { 27 }; //x3类型是std::initializer_list<int>(对auto,要求元素须是相同类型),不是int
- initializer常量不能用于void f(T param);的模板参数类型推导,但声明为void f(std::initializer_list<T>)可以
- C++14允许函数返回值类型的推导,用的是template type deduction,不是auto type deduction
- 理解decltype
- bool f(const Widget& w); // decltype(w) is const Widget&, decltype(f) is
bool(const Widget&)
- 注意,函数类型的写法(ReturnType(ArgsType))之前似乎没见过,好像是后来流行的?
- 主要用途:声明依赖于函数参数的返回值类型
- auto f(Container& c, Index i) -> decltype(c[i]) { ... }
- 完善为 -> decltype(std::forward<Container>(c)[i])
- 但operator[]可能对std::vector<bool>特化模板容易出问题
- auto f(Container& c, Index i) -> decltype(c[i]) { ... }
- C++14 decltype(auto):可以正确处理引用类型的推导(因为decltype对左值类型总是返回T&)
- decltype(auto) f(Container&& c, Index i) { ...return std::forward<Container>(c)[i]; }
- 对int x=0; decltype(x)得到int,decltype((x))返回int&
- bool f(const Widget& w); // decltype(w) is const Widget&, decltype(f) is
bool(const Widget&)
- 怎样在IDE里查看编译器推导的类型
auto
- 优先使用auto
- std::function比auto慢?(总感觉这是编译器优化的问题)
- v.size()是std::vector<int>::size_type类型(unsigned),使用auto可避免这个记忆负担?
- for (const auto& p : m) { ... } //为什么不是for(auto p : m)?
- 所谓的可不见的内部代理类:auto highPriority = static_cast<bool>(features(w)[5]);
Moving to Modern C++
- 区别()与{}(初始化对象)
- 如果类定义了std::initializer_list构造函数的话,{}优先匹配(不管实际的参数类型)//C++编译器的灵活性有点缺乏
- 用空std::initializer_list初始化:Widget w4({}); //直接Widget w4{};调用默认构造
- 变参模板(这让我想起了Scheme syntax-rules变态的模式匹配语法)
- template<typename T, typename... Ts>
- void doSomeWork(Ts&&... params){
- T localObject(std::forward<Ts>(params)...);
- 如果类定义了std::initializer_list构造函数的话,{}优先匹配(不管实际的参数类型)//C++编译器的灵活性有点缺乏
- Prefer nullptr to 0 and NULL(这样的条款有点侨情)
- f(nullptr); // calls f(void*) overload
- Prefer alias declarations to typedef's
- 别名声明:using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
- using FP = void (*)(int, const std::string&);
- template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>;
- 老实的写法:需要typename告诉编译器内部名字是一个类型不是变量,没有using好?
- 基于<type_traits>的类型特性擦除:
- std::remove_const<T>::type
- std::remove_reference<T>::type
- 别名声明:using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
- Prefer scoped enums to unscoped enums
- enum class Color { black, white, red }; //但这样一来,引用枚举量时都需要那个枚举类名前缀
- enum class Status: std::uint32_t;
- =delete
- overriding发生的前提:***
- 基类需要声明virtual
- 函数名必须相同(废话)
- 参数类型必须相同(*)
- const性质必须相同(*)
- 返回类型和异常规格必须兼容(*)
- C++11:引用限定必须相同:void f() &; void f() &&;(代表f只能在左值/右值对象上调用)
- override是上下文关键字(C++11特性),可用作函数名称
- Prefer const_iterators to iterators
- 作者又在强调常量迭代器了,但实际上,STL的非常量迭代器通常更有效
- 定义非成员版本的cbegin://优先非成员只不过是作者偏好FP风格的体现
- template <class C> auto cbegin(const C& container)->decltype(std::begin(container)) { ... }
- 注意,const类型修饰只是为了让编译器帮助我们完成一些编译器的类型安全检查
- template <class C> auto cbegin(const C& container)->decltype(std::begin(container)) { ... }
- noexcept
- vs C++98 throw() //差别在发生异常时的stack unwinding处理上,略
- 有条件的noexcept:noexcept(noexcept(swap(*a, *b)))
- constexpr
- 代表const in const out,可用作编译期的常量
- C++11里对函数的一些限制:略(这只不过是编译器的实现问题,C++有时候老是会把语言、库与编译器实现混淆在一起!)
- *C++14:setter也可以是constexpr
- const成员函数应该是线程安全的
- 可以修改mutable数据成员
- 如果要一次修改多个atomic,使用mutex保护一致性:std::lock_guard<std::mutex> g(m);
- 理解特殊的成员函数生成
- copy操作(构造&赋值)、move操作
- C++11里,如果用户定义了dtor(析构函数),则move操作不会自动生成 => 可使用=default绕过
- Member function templates never suppress generation of special member functions(C++里的偏僻规则太多了)
- 有些规则只对库设计者才有意义,使用者(App开发者)只需遵循编码规范就可以了
Smart Pointers
- C++11:
std::auto_ptr, std::unique_ptr, std::shared_ptr, and std::weak_ptr - std::unique_ptr
- 定制deleter:
- auto delA = [](A* a){ ... delete a;}
- std::unique_ptr<A, decltype(delA)> makeA(...){ ... }
- 转换为std::shared_ptr
- 定制deleter:
- std::shared_ptr
- 通常,保存引用计数的'控制块是动态分配的
- 但std::make_shared将控制块与对象内存绑定到一起,这给对象的真正释放内存带来了麻烦
- 对比:std::shared_ptr<Widget> spw1(pw, loggingDel); //创建一个新的控制块
- 奇特递归模板:class Widget: public std::enable_shared_from_this<Widget> { ... }
- shared_from_this()之前需要已有std::shared_ptr引用
- ==> private ctor,+定义static create方法
- shared_from_this()之前需要已有std::shared_ptr引用
- 通常,保存引用计数的'控制块是动态分配的
- std::weak_ptr
- 弱引用指针通常与引用计数指针配合使用:std::weak_ptr<Widget> wpw(spw);
- if (wpw.expired()) ...
- auto spw2 = wpw.lock();
- 实现细节:std::shared_ptr的控制块中存储了弱引用计数?
- 弱引用指针通常与引用计数指针配合使用:std::weak_ptr<Widget> wpw(spw);
- std::make_unique和std::make_shared
- 潜在的资源泄漏:processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
- ==> processWidget(std::make_shared<Widget>(), computePriority());
- 潜在的资源泄漏:processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
- 当使用Pimpl习俗时,需要定义特殊的成员函数(dtor及move操作)
- 定制deleter是std::unique_ptr类型的一部分,但不是std::shared_ptr类型的一部分
Rvalue References, Move Semantics, and Perfect Forwarding
- 即使声明为void f(T&& a); 参数总是左值
- 理解std::move和std::forward
- std::move和std::forward既不移动也不转发,只是类型转换(运行时什么也不做)
- std::move无条件把参数转换为右值,而std::forward要求实参必须是右值(注意,形参总是左值)
- move构造函数的参数是非const的,但copy构造函数的参数是const的,这一差别允许const右值实参调用copy构造
- std::move总是可以换成std::forward
- 区别‘通用引用’和右值引用
- 不是右值引用:
- void f(T&& param);
- auto&& var2 = var1; //前提:存在类型推导
- 右值引用:
- void f(Widget&& param);
- Widget&& var1 = Widget();
- void f(std::vector<T>&& param);
- void f(const T&& param);
- 即使写成T&&但不存在类型推导的情况(从类模板实例化得到的成员函数 vs 成员函数模板)
- C++14:
- [](auto&& func, auto&&... params){ std::forward<decltype(func)>(func)(std::forward<decltype(params)>(params)...); }
- 不是右值引用:
- 在右值引用上使用std::move,在通用引用上使用std::forward
- return std::move(lhs); //不发生“copy到临时对象”,注意lhs是‘通用引用’(T&&类型参数)
- 不要return std::move局部对象(这种情况下反而无法RVO)
- 编译器能够做RVO,即使你不写成return Widget();?
- 这里面有点扯淡,因为仅仅是“C++标准委员会”这么说的,作者似乎混淆了TMP(模板元编程)和普通的写法
- 编译器能够做RVO,即使你不写成return Widget();?
- Avoid overloading on universal references
- copy构造函数的参数总是const的,一个非const的参数将优先匹配universal reference构造函数模板
- tag dispatch?脑抽!
- std::enable_if
- typename = typename std::enable_if<condition>::type //靠,真是疯狂
- typename std::decay<T>::type:去除类型T的引用、const、volatile修饰
- condition:!std::is_same<Person, typename std::decay<T>::type>::value
- C++14语法:std::decay_t<T>(不必再写typename)
- std::is_base_of<T1, T2>::value
- 理解引用塌陷
- 不允许用户声明引用的引用,但是编译器可以这么做?只要有一个是lvalue引用,结果就是左值引用
- 4个上下文:模板实例化、auto类型推导、typedef和using类型别名、decltype
- 完美转发
- template<typename T> void fwd(T&& param){ f(std::forward<T>(param)); }
- template<typename... Ts> void fwd(T&&... param){ f(std::forward<T>(param)...); }
- 失败的情况:(无法类型推导,或不正确的推导)
- braced initializer:void f(const std::vector<int>& v); ==> fwd({ 1, 2, 3 });
- 0 or NULL as null pointers
- 重载的函数指针、模板函数 ==> 手工地指定函数重载或实例化版本
- struct中的bit域(无法以非const寻址)
- p211 references are simply pointers that are automatically dereferenced
Lambda Expressions
- 避免默认捕获
- using FilterContainer = std::vector<std::function<bool(int)>>;
- filters.emplace_back( [](int value) { return value % 5 == 0; } );
- [&] 与 [=]
- 不能捕获成员变量,捕获的是this指针(隐式)
- 全局变量和static局部变量是引用不是捕获,捕获真针对局部变量(?)
- Use C++14 init capture to move objects into closures
- auto pw = std::make_unique<Widget>();
- auto func = [pw = std::move(pw)] { ... }
- => auto func = [pw = std::make_unique<Widget>()] { ... }
- move capture的C++11仿真:
- auto func = std::bind( [](const std::vector<double>& data){...}, std::move(data) );
- Chromium代码里有类似的写法:void InProcessCommandBuffer::Destroy() {
- base::WaitableEvent completion(true, false);
- bool result = false;
- base::Callback<bool(void)> destroy_task = base::Bind( &InProcessCommandBuffer::DestroyOnGpuThread, base::Unretained(this));
- QueueTask(base::Bind(&RunTaskWithResult<bool>, destroy_task, &result, &completion));
- mutable lambda?(见鬼,之前怎么没见过这种语法)
- Use decltype on auto&& parameters to std::forward them
- C++14 泛型lambda?:auto f = [](auto x){ return func(normalize(x)); };
- 完美转发:
- auto f = [](auto&& param){ return func(normalize(std::forward<decltype(param)>(param))); };
- variadic的版本:略
- Prefer lambdas to std::bind
- bind对象(实际上是一个函数对象,注意,lambda也是!)
- lambda更可读(这倒是真的,bind有点像FP里的currying)
- p233 C++14 using namespace std::literals; //特殊的类型常量后缀语法,如1h 30s
- bind传参now()的话,传的是bind时的时间,而不是目标函数被调用时的时间(嗯,这让我想起了JavaScript里类似的问题)
- 推迟求值:auto setSoundB = std::bind(setAlarm,
- std::bind(std::plus<>(), steady_clock::now(), 1h), _1, 30s);
- 注意这里的语法,bind嵌套了bind以惰性求值
- 推迟求值:auto setSoundB = std::bind(setAlarm,
- std::bind总是复制参数,但可以用std::ref来传引用:
- auto compressRateB = std::bind(compress, std::ref(w), _1);
- std::bind可接受任意类型,假设PolyWidget::operator()是成员模板,则可:auto boundPW = std::bind(pw, _1);
- C++11 lambda:无等价形式
- C++14:auto boundPW = [pw](const auto& param){ pw(param); }; //但这种多态lambda有实际用途吗
The Concurrency API
- Prefer task-based programming to thread-based
- auto futureResult = std::async(doAsyncWork);
- 线程:硬件(CPU)、软件(OS)、std::thread对象
- 即使函数不抛异常:int doAsyncWork() noexcept;,std::thread t(doAsyncWork);也有可能因为资源不足抛错
- oversubscription问题(增加了调度开销)
- std::async有可能在当前线程里执行doAsyncWork(异步的形式,实质却是同步函数调用),避免了线程过载
- GUI线程响应问题:显示指定std::launch::async启动策略
- 话说这里的讨论似乎与Chromium impl-side painting的目标有相通之处。。。
- Specify std::launch::async if asynchronicity is essential
- 启动策略:std::launch::async 或 std::launch::deferred(直到在future上get/wait,此时同步执行)
- if (fut.wait_for(0s) == std::future_status::deferred){ //那么直接同步调用?
- 作者怎么这么喜欢写wrapper函数?std::result_of<F(Ts...)> ...
- Make std::threads unjoinable on all paths
- implicit join:线程对象的析构函数会等待线程的执行完成(C++的这个API设计其实是有问题的,应该像Java那样提供显式的start方法)
- ~ThreadRAII():if (t.joinable()) { t.join() or detach() }
- Be aware of varying thread handle destructor behavior
- 线程执行结果:不能存在callee的std::promise,也不能是caller的std::future(可转换为shared_future)
- ==> shared state(实际上存放在堆里)
- The normal behavior is that a future’s destructor destroys the future object. That’s it.
- ?
- std::packaged_task(略)
- The final future referring to a shared state for a non-deferred task launched via std::async blocks until the task completes
- 这话说得太拗口了,其实不就是在析构函数里阻塞等待嘛
- 线程执行结果:不能存在callee的std::promise,也不能是caller的std::future(可转换为shared_future)
- Consider void futures for one-shot event communication
- std::condition_variable cv; //条件变量上wait有可能导致CPU繁忙吗?
- std::mutex m; //条件变量需要互斥访问,用mutex来保护
- 通知方:cv.notify_one();
- 等待方:{std::unique_lock<std::mutex> lk(m); cv.wait(lk); ...}
- 问题:
- mutex是否必需?
- wait之前notify将导致hang
- spurious wakeups:wait不是由于notify(这种情况下需要把wait放到while循环里?)
- 由于只通知一次,考虑使用一个开关状态变量:std::atomic<bool> flag(false);
- while (!flag); //但是这样会导致忙等
- 使用普通bool flag; 但是用mutex加锁:
- {std::lock_guard<std::mutex> g(m); flag = true;} cv.notify_one();
- {std::unique_lock<std::mutex> lk(m); cv.wait(lk, [] { return flag; }); ...} //避免了notify信号丢失的问题
- caller-callee一次通信:使用future-promise
- std::promise<void> p;
- 通知方:p.set_value();
- 等待方:p.get_future().wait(); //没有mutex?那就是说没有加锁,那么底层是怎么做到的?
- 注意,promise和future之间是一个动态堆分配的shared_state
- p269 如果发生了异常的情况?(C++这种命令式语言要在异常情况下仍然能够保证事务处理的一致性恐怕有点困难)
- auto sf = p.get_future().share(); ...(下略)
- Use std::atomic for concurrency, volatile for special memory
- volatile:不能保证读与写操作的原子性,并且‘数据竞争’可导致无法预测/未定义的行为(编译器可生成任意指令?)
- std::atomic限制了指令重排:写操作完成之前,不允许后续指令提前执行(感觉这更像是Java里的volatile?)
- volatile std::atomic<int> vai;
Tweaks
- Consider pass by value for copyable parameters that are cheap to move and always copied
- 重载 const std::string& 和 std::string&&
- T&& 加 std::forward<T>
- std::string传值 加 std::move
- C++98:经典的slicing问题(指的是一个子类对象传值给一个基类形参)
- Item 42: Consider emplacement instead of insertion
- 42,不错的数字~
- 直接传递一个字符串常量给一个std::string&参数?临时对象析构和std::move开销
- vs.emplace_back("xyzzy"); //使用完美转发,直接在vector<string>容器内部构造一个string对象
- 嗯,把它称为‘就地插入’
- vs.emplace_back("xyzzy"); //使用完美转发,直接在vector<string>容器内部构造一个string对象
- heuristics:
- The value being added is constructed into the container, not assigned
- The argument type(s) being passed differ from the type held by the container
- The container is unlikely to reject the new value as a duplicate
- 对shared_ptr不适用vp.emplace_back( new Widget())?
- 但是如果先构造td::shared_ptr<Widget> spw(new Widget, killWidget);的话,则push_back、emplace_back无差别
- 与explicit构造函数的交互
- 可以regexes.emplace_back(nullptr);,那是因为regex允许从nullptr explicit构造
- std::regex r(nullptr); 可以通过编译,但是属‘未定义行为’
- 注意,前者是直接初始化,而std::regex r=nullptr;写法是拷贝初始化
- 可以regexes.emplace_back(nullptr);,那是因为regex允许从nullptr explicit构造