- chapter 1 -
item 1 理解模板类型推断 rem
对于引用(模板参数类型是引用)推导时,有引⽤的实参会被视为⽆引⽤,他们的引⽤会被忽略(之后拼接上模板类型的&(若有的话))
对于通⽤引⽤的推导,左值实参会被特殊对待
对于传值类型推导,实参如果具有常量性和易变性会被忽略
在模板类型推导时,数组或者函数实参会退化为指针,除⾮它们被⽤于初始化引⽤
item 2 理解auto推断 对于花括号的处理是auto类型推导和模板类型推导唯⼀不同的地⽅。当使⽤auto的变量使⽤花括号的语, 法进⾏初始化的时候,会推导出std::initializer_list的实例化,但是对于模板类型推导这样就⾏不通:
1 2 3 4 5 6 7 8 9 10 auto x={11 ,23 ,9 }; template <typename T> void f (T param) ; f ({11 ,23 ,9 }); template <typename T> void f (std::initializer_list<T> initList) ; f ({11 ,23 ,9 });
以上是cpp11的,但是cpp14中允许函数的形参和返回值都为auto,但是推断还是模板推断那一套:
1 2 3 std::vector<int > v; auto resetV = [&v](const auto & newValue){v=newValue;}; reset ({1 ,2 ,3 });
rem
auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表
std::initializer_list而模板类型推导不这样做 在C++14中auto允许出现在函数返回值或者lambda函数形参中,但是它的⼯作机制是模板类型推 导那⼀套⽅案。
item 3 理解decltype 1 使用它获取你想要的的类型 1 2 3 4 5 6 7 8 9 10 11 template <typename Container,typename Index> auto authAndAccess (Container& c,Index i) { authenticateUser (); return c[i]; }std::deque<int > d; ... authAndAccess (d,5 )=10 ; template <typename Container,typename Index> decltype (auto ) authAndAccess (Container&& c,Index i) { authenticateUser (); return std::forward<Container>(c)[i]; }
上⾯的代码尝试把10赋值给右值,C++11禁⽌这样做,所以代码⽆法编译。 下面的改进版本,同时使用万能引用传入左值和右值的功能。
2 唯一需要注意的点 当使⽤decltype(auto) 的时候⼀定要加倍的小⼼,在表达式中看起来⽆⾜轻重的细节将会影响到类型的 推导。为了确认类型推导是否产出了你想要的结果,请参⻅Item4描述的那些技术。
1 2 3 4 5 6 7 8 9 int x;decltype (auto ) f1 () { int x = 0 ; ... return x; }decltype (auto ) f2 () { int x =0l ; return (x); }
rem
decltype总是不加修改的产⽣变量或者表达式的类型。
对于T类型的左值表达式,decltype总是产出T的引⽤即T&。
C++14⽀持decltype(auto) ,就像auto⼀样,推导出类型,但是它使⽤⾃⼰的独特规则进⾏推 导。
item 4 学会看推导出来的类型 rem
类型推断可以从IDE看出,从编译器报错看出,从⼀些库的使⽤看出
这些⼯具可能既不准确也⽆帮助,所以理解C++类型推导规则才是最重要的
– chapter 2 –
item 5 优先考虑auto而不是显式类型 ⾸先,深呼吸,放松,auto是可选项,不是命令,在某些情况下如果你的专业判断告诉你使⽤显式类型 声明⽐auto要更清晰更易维护,那你就不必再坚持使⽤auto。
1 2 3 4 5 6 7 8 9 10 std::unordered_map<std::string,int > m; ... for (const std::pair<std::string,int >& p : m) { ... }for (const auto & p : m) { ... }
rem
auto变量必须初始化,通常它可以避免⼀些移植性和效率性的问题,也使得重构更⽅便,还能让你 少打⼏个字。
正如Item2和6讨论的,auto类型的变量可能会踩到⼀些陷阱。
item 6 若非己愿,用显示类型而不是auto auto错误推到:
1 2 3 4 5 6 7 8 9 10 bool highPriority = features (w)[5 ]; auto highPriority = features (w)[5 ]; processWidget (w,highPriority);
作为⼀个通则,不可⻅的代理类通常不适⽤于auto。
当你不知道这个类型有没有被代理还想使⽤auto时你就不能单单只⽤⼀ 个auto。auto本⾝没什么问题,问题是auto不会推导出你想要的类型。 解决⽅案是强制使⽤⼀个不同的 类型推导形式,这种⽅法我通常称之为显式类型初始器惯⽤法(the explicitly typed initialized idiom)
1 auto highPriority = static_cast <bool >(features (w)[5 ]);
rem
不可⻅的代理类可能会使auto从表达式中推导出“错误的”类型
显式类型初始器惯⽤法强制auto推导出你想要的结果
— chapter 3 — cpp11/14新特性
item 7 区别使⽤()和{}创建对象 cpp11使用同一初始化,只有花括号任何地方都能用 仅仅阐述几个平时少见的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::vector<int > ai1{0 }; std::atomic<int > ai2 (0 ) ; std::atomic<int > ai3 = 0 ; double x,y,z; int sum1{x+y+z}; int sum2 (x + y +z) ; int sum3 = x + y + z; Widget w1 (10 ) ; Widget w2 () ; Widget w3{};
但是你越喜欢⽤atuo,你就越不能⽤括号初始化,因为编译器热衷于把括号初始化与使 std::initializer_list构造函数匹配了,热衷程度甚⾄超过了最佳匹配, 甚⾄普通的构造函数和移动构造函数都会被std::initializer_list构造函数劫持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Widget { public : Widget (int i, bool b); Widget (int i, double d); Widget (std::initializer_list<long double > il); }; Widget w1 (10 , true ) ; Widget w2{10 , true }; Widget w3 (10 , 5.0 ) ; Widget w4{10 , 5.0 }; Widget w5 (w4) ; Widget w6{w4}; Widget w7 (std::move(w4)) ; Widget w8{std::move (w4)}; class Widget { public : Widget (int i, bool b); Widget (int i, double d); Widget (std::initializer_list<bool > il); Widget w{10 , 5.0 }; class Widget { public : Widget (); Widget (std::initializer_list<int > il); ... }; Widget w1; w2{}; Widget w3 () ; Widget w4 ({}) ; w5{{}}; std::vector<int > v1 (10 , 20 ) ; std::vector<int > v2{10 , 20 };
关于花括号和小括号的使⽤没有⼀个⼀致的观点,所以我的建议是⽤⼀个,并坚持使⽤。
rem
括号初始化是最⼴泛使⽤的初始化语法,它防⽌变窄转换,并且对于C++最令⼈头疼的解析有天⽣ 的免疫性
在构造函数重载决议中,括号初始化尽最⼤可能与std::initializer_list参数匹配,即便其他构造函数 看起来是更好的选择
对于数值类型的std::vector来说使⽤花括号初始化和小括号初始化会造成巨⼤的不同
在模板类选择使⽤小括号初始化或使⽤花括号初始化创建对象是⼀个挑战。
item 8 优先考虑nullptr而⾮0和NULL nullptr的优点是它不是整型,同时也可以使代码表意明确,尤其是当和auto⼀起使⽤时。⽼实说它也不是⼀个指针类型,但是你可以把它认为是通⽤类型的指针。 nullptr的真正类型是std::nullptr_t,在⼀个完美的循环定义以后,std::nullptr_t⼜被定义为nullptr。 当模板出现时nullptr就更有⽤了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int f1 (std::shared_ptr<Widget> spw) ; double f2 (std::unique_ptr<Widget> upw) ; bool f3 (Widget* pw) ;std::mutex f1m, f2m, f3m; template <typename FuncType, typename MuxType, typename PtrType> decltype (auto ) lockAndCall ( FuncType func, MuxType& mutex, PtrType ptr) { MuxGuard g (mutex) ; return func (ptr); } auto result1 = lockAndCall (f1, f1m, 0 ); auto result2 = lockAndCall (f2, f2m, NULL ); auto result3 = lockAndCall (f3, f3m, nullptr );
rem
优先考虑nullptr而⾮0和NULL
避免重载指针和整型(因为在cpp98中,绝对会把0当成int,而你用0当成空指针指望着去调用重载的指针函数,你会失败)
item 9 优先考虑别名声明而⾮typedef 简单例子:
1 2 3 4 typedef void (*FP) (int , const std::string&) ; using FP = void (*)(int , const std::string&);
不过有⼀个地⽅使⽤别名声明吸引⼈的理由是存在的:模板。特别的,别名声明可以被模板化但是typedef不行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <typename T> struct MyAllocList { typedef std::list<T, MyAlloc<T>> type; }; MyAllocList<int >::type lw; template <typename T> class Widget { private : typename MyAllocList<T>::type list; … }; template <typename T> using MyAllocList = std::list<T, MyAlloc<T>>;template <typename T> class Widget { private : MyAllocList<T> list; … };
如果你尝试过模板元编程(TMP), 你⼀定会碰到取模板类型参数然后基于它创建另⼀种类型的情况。 举个例⼦,给⼀个类型T, 如果你想去掉T的常量修饰和引⽤修饰,⽐如你想把const std::string&变成const std::string。尽管写了⼀些,但我这⾥不是想给你⼀个关于type traits使⽤的教程。注意类型转换尾部的::type。 如果你在⼀个模板内部使⽤类型参数,你也需要在它们前⾯加上typename。 因为标准委员会没有及时 认识到别名声明是更好的选择,所以直到C++14它们才提供了使⽤别名声明的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std::remove_const<T>::type std::remove_const_t <T> std::remove_reference<T>::type std::remove_reference_t <T> std::add_lvalue_reference<T>::type std::add_lvalue_reference_t <T> template <class T >using remove_const_t = typename remove_const<T>::type; template <class T >using remove_reference_t = typename remove_reference<T>::type; template <class T >using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;
rem
typedef不⽀持模板化,但是别名声明⽀持。
别名模板避免了使⽤”::type”后缀,而且在模板中使⽤typedef还需要在前⾯加上typename
C++14提供了C++11所有类型转换的别名声明版本
item 10 优先考虑限域枚举而⾮未限域枚举 限域枚举相比于非限域枚举的优点如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 enum Color { black, white, red }; auto white = false ; enum class Color { black, white, red }; auto white = false ; Color c = white; Color c = Color::white; auto c = Color::white; enum Color { black, white, red }; std::vector<std::size_t > primeFactors (std::size_t x); Color c = red; … if (c < 14.5 ) { auto factors = primeFactors (c); … } enum class Color { black, white, red }; Color c = Color::red; … if (c < 14.5 ) { auto factors = primeFactors (c); … } if (static_cast <double >(c) < 14.5 ) { auto factors = (static_cast <std::size_t >(c)); … } enum class Status { good = 0 , failed = 1 , incomplete = 100 , corrupt = 200 , audited = 500 , indeterminate = 0xFFFFFFFF }; enum class Status ; continueProcessing (Status s); using UserInfo = std::tuple<std::string, std::string, std::size_t > ; enum class UserInfoFields { uiName, uiEmail, uiReputation }; UserInfo uInfo; … auto val = std::get<static_cast <std::size_t >(UserInfoFields::uiEmail)> (uInfo); template <typename E> constexpr auto toUType (E enumerator) noexcept { return static_cast <std::underlying_type_t <E>>(enumerator); }auto val = std::get <toUType (UserInfoFields::uiEmail)>(uInfo);
rem
C++98的枚举即⾮限域枚举
限域枚举的枚举名仅在enum内可⻅。要转换为其它类型只能使⽤cast。
⾮限域/限域枚举都⽀持基础类型说明语法,限域枚举基础类型默认是 int,⾮限域枚举没有默认 基础类型。
限域枚举总是可以前置声明。⾮限域枚举仅当指定它们的基础类型时才能前置。
item 11 优先考虑使⽤deleted函数而⾮使⽤未定义的私有声明 你想要禁止客户使用某些函数代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <class charT , class traits = char_traits<charT> > class basic_ios : public ios_base { public : … basic_ios (const basic_ios& ) = delete ; basic_ios& operator =(const basic_ios&) = delete ; … }; bool isLucky (int number) ; bool isLucky (char ) = delete ; bool isLucky (bool ) = delete ; bool bool isLucky (double ) = delete ;
禁止一些模板实例化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <typename T> void processPointer (T* ptr) ;template <> void processPointer <void >(void *) = delete ; template <> void processPointer <char >(char *) = delete ;template <> void processPointer <const void >(const void *) = delete ; template <> void processPointer <const char >(const char *) = delete ;class Widget { public : … template <typename T> void processPointer (T* ptr) { … } private : template <> void processPointer <void >(void *); }; class Widget { public : … template <typename T> void processPointer (T* ptr) { … } … }; template <> void Widget::processPointer <void >(void *) = delete ;
rem
⽐起声明函数为private但不定义,使⽤delete函数更好
任何函数都能 delete ,包括⾮成员函数和模板实例
item 12 使⽤override声明重载函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Base { public :void doWork () & ; void doWork () && ; virtual void mf1 () const ; virtual void mf2 (int x) ; virtual void mf3 () & ; void mf4 () const ; }; class Derived : public Base { public : virtual void mf1 () override ; virtual void mf2 (unsigned int x) override ; virtual void mf3 () && override ; void mf4 () const override ; }; class Widget { public : using DataType = std::vector<double >; … DataType& data () & { return values; } DataType data () && { return std::move (values); } private : DataType values; }; auto vals1 = w.data (); auto vals2 = makeWidget ().data ();
rem
为重载函数加上 override
成员函数限定让我们可以区别对待左值对象和右值对象(即 *this )
item 13 优先考虑const_iterator而⾮iterator rem
优先考虑const_iterator而⾮iterator
在最⼤程度通⽤的代码中,优先考虑⾮成员函数版本的begin,end,rbegin等,而⾮同名成员函 数(因为c11并没有cbegin,那么你可以手动写一个cbegin)
item 14 如果函数不抛出异常请使⽤noexcept 函数是否为noexcept和成员函数是否const⼀样重要。如果知道这个函数不会抛异常就 加上noexcept是简单天真的接口说明。 不过这⾥还有给不抛异常的函数加上noexcept的动机:它允许编译器⽣成更好的⽬标代码。
1 2 3 RetType function (params) noexcept ; RetType function (params) throw () ; RetType function (params) ;
希望你能为noexcept提供的优化机会感到⾼兴,同时我还得让你缓⼀缓别太⾼兴了。优化很 重要,但是正确性更重要。些函数很⾃然的不应该抛异常, 更进⼀步值得注意的是移动操作和swap——使其不抛异常有重 ⼤意义,只要可能就应该将它们声明为noexcept。或者像是vector的push_back在发生扩容(能移动就移 动,必要时就复制)的时候,最后一个移动产生异常了,那么导致push_back失败,然后移动了的vec也不容易返回到原来的位置,
rem
在C++98构造函数和析构函数抛出 异常是糟糕的代码设计
noexcept是函数接口的⼀部分,这意味着调⽤者会依赖它、
noexcept函数较之于⾮noexcept函数更容易优化
noexcept对于移动语义,swap,内存释放函数和析构函数⾮常有⽤ ⼤多数函数是异常中⽴的(译注:可能抛也可能不抛异常)而不是noexcept
item 15 尽可能的使⽤constexpr constexpr表明⼀个值不仅仅是常量,还是编译期可知的。这个表述并不全⾯,因为当constexpr被⽤于函数的时候,事情就有⼀些细微差别了。 你不能假设constexpr函数是const,也不能保证 它们的(译注:返回)值是在编译期可知的。最有意思的是,这些是特性。关于constexpr函数返回的 结果不需要是const,也不需要编译期可知这⼀点是良好的⾏为。
1 2 3 4 5 6 7 8 9 10 int sz; … constexpr auto arraySize1 = sz; std::array<int , sz> data1; constexpr auto arraySize2 = 10 ; std::array<int , arraySize2> data2; int sz; std::array<int , arraySize> data;
constexpr相当于宣称“我能在C++要求常量表达式的地⽅使⽤它”
rem
constexpr对象是cosnt,它的值在编译期可知 当传递编译期可知的值时,
cosntexpr函数可以产出编译期可知的结果(越多这样的代码,运行时就会少一些不必要的计算,你的代码就越快)
item 16 让const成员函数线程安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class Polynomial {public : using RootsType = std::vector<double >; RootsType roots () const { if (!rootsAreValid) { … rootsAreValid = true ; } return rootVals; } private : mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; }; auto rootsOfp = p.roots (); auto valsGivingZero = p.roots ();class Widget {public : … int magicValue () const { std::lock_guard<std::mutex> guard (m) ; if (cacheValid) return cachedValue; else { auto val1 = expensiveComputation1 (); auto val2 = expensiveComputation2 (); cachedValue = val1 + val2; cacheValid = true ; return cachedValue; } } … private : mutable std::mutex m; mutable int cachedValue; mutable bool cacheValid{ false }; };
rem
确保const成员函数线程安全,除⾮你确定它们永远不会在临界区(concurrent context)中 使⽤。
std::atomic 可能⽐互斥锁提供更好的性能,但是它只适合操作单个变量或内存位置。
item 17 理解特殊成员函数的⽣成 C++11特殊成员函数俱乐部迎来了两位新会员:移动构造函数和移动赋值运算符。 先简单记住如果⽀持移动就会逐成员移动类成员和基类成 员,如果不⽀持移动就执⾏拷⻉操作就好了,item23会更新!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Widget {public : … Widget (); ~Widget (); Widget (Widget& w); Widget& operator =(Widget& rhs); Widget (Widget&& rhs); Widget& operator =(Widget&& rhs); … }; class Widget { public : … ~Widget (); … Widget (const Widget&) = default ; Widget& operator =(const Widget&) = default ; … }; class StringTable {public : StringTable () { makeLogEntry ("Creating StringTable object" ); } ~StringTable () { makeLogEntry ("Destroying StringTable object" ); } … private : std::map<int , std::string> values; };
rem
特殊成员函数是编译器可能⾃动⽣成的函数:默认构造,析构,拷⻉操作,移动操作。
移动操作仅当类没有显式声明移动操作,拷⻉操作,析构时才⾃动⽣成。
拷⻉构造仅当类没有显式声明拷⻉构造时才⾃动⽣成,并且如果⽤⼾声明了移动操作,拷⻉构造就 是delete。
拷⻉赋值运算符仅当类没有显式声明拷⻉赋值运算符时才⾃动⽣成,并且如果⽤⼾声明 了移动操作,拷⻉赋值运算符就是delete。当⽤⼾声明了析构函数,拷⻉操作不再⾃动⽣成。
—- chapter 4 —- 智能指针 item 18 对于独占资源使⽤std::unique_ptr 如果原始指针够小够快,那么 std::unique_ptr ⼀样可以。可以移动但是不允许拷贝,不然每个都认为⾃⼰拥有资源,销 毁时就会出现重复销毁。 且unique_ptr适用于工厂函数返回新产生的对象的指针,因为我们并不知道产生的新对象是需要被专有化还是共享,unique_ptr转到shared_ptr非常方便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 class Investment { ... }; class Sock : public Investment {...}; class Bond : public Investment {...}; class RealEstate : public Investment {...};template <typename ... Ts> std::unique_ptr<Investment> makeInvestment (Ts&&... params);{ … auto pInvestment = makeInvestment ( arguments ); … } auto delInvmt = [](Investment* pInvestment) { makeLogEntry (pInvestment); delete pInvestment; }; template <typename ... Ts>std::unique_ptr<Investment, decltype (delInvmt)> makeInvestment (Ts&&... params){ std::unique_ptr<Investment, decltype (delInvmt)> pInv (nullptr , delInvmt); if () { pInv.reset (new Stock (std::forward<Ts>(params)...)); } else if ( ) { pInv.reset (new Bond (std::forward<Ts>(params)...)); } else if ( ) { pInv.reset (new RealEstate (std::forward<Ts>(params)...)); } return pInv; } template <typename ... Ts>auto makeInvestment (Ts&&... params) { auto delInvmt = [](Investment* pInvestment) { makeLogEntry (pInvestment); delete pInvestment; }; ... } auto delInvmt1 = [](Investment* pInvestment) { makeLogEntry (pInvestment); delete pInvestment; }; template <typename ... Ts> std::unique_ptr<Investment, decltype (delInvmt1)> makeInvestment (Ts&&... args);void delInvmt2 (Investment* pInvestment) { makeLogEntry (pInvestment); delete pInvestment; } template <typename ... Ts> std::unique_ptr<Investment, void (*)(Investment*)> makeInvestment (Ts&&... params); std::shared_ptr<Investment> sp = makeInvestment (arguments);
rem
std::unique_ptr 是轻量级、快速的、只能move的管理专有所有权语义资源的智能指针
默认情况,资源销毁通过delete,但是⽀持⾃定义delete函数。有状态的删除器和函数指针会增加
std::unique_ptr 的⼤小 将 std::unique_ptr 转化为 std::shared_ptr 是简单的
item 19 对于共享资源使⽤std::shared_ptr std::shared_ptr 通过引⽤计数来确保它是否是最后⼀个指向某种资源的指针,引⽤计数关联资源并跟 踪有多少 std::shared_ptr 指向该资源。 std::shared_ptr 构造函数递增引⽤计数值(注意是通常,原因是移动构造函数的存在。从另⼀个 std::shared_ptr 移动构造新 std::shared_ptr 会将原来的 std::shared_ptr 设置为null,那意味着⽼的 std::shared_ptr 不再指向资源,同时新的 std::shared_ptr 指向资源。这样的结果就是不需要修改引⽤计数值。因此移动 std::shared_ptr 会 ⽐拷⻉它要快:拷⻉要求递增引⽤计数值,移动不需要。移动赋值运算符同理,所以移动赋值运算符也 ⽐拷⻉赋值运算符快。),析构函数递减值,拷⻉赋值运算符可能递增也可能递减值。(如果sp1和sp2是 std::shared_ptr 并且指向不同对象,赋值运算符 sp1=sp2 会使sp1指向sp2指向的对象。直接效果就 是sp1引⽤计数减⼀,sp2引⽤计数加⼀。) 引⽤计数暗⽰着性能问题:
std::shared_ptr ⼤小是原始指针的两倍,因为它内部包含⼀个指向资源的原始指针,还包含⼀ 个资源的引⽤计数值。
引⽤计数必须动态分配。 理论上,引⽤计数与所指对象关联起来,但是被指向的对象不知道这件事情(译注:不知道有指向⾃⼰的指针)。因此它们没有办法存放⼀个引⽤计数值。Item21会解释使 ⽤ std::make_shared 创建 std::shared_ptr 可以避免引⽤计数的动态分配,但是还存在⼀些 std::make_shared 不能使⽤的场景,这时候引⽤计数就会动态分配。
递增递减引⽤计数必须是原⼦性的,因为多个reader、writer可能在不同的线程。⽐如,指向某种 资源的 std::shared_ptr 可能在⼀个线程执⾏析构,在另⼀个不同的线程, std::shared_ptr 指 向相同的对象,但是执⾏的确是拷⻉操作。原⼦操作通常⽐⾮原⼦操作要慢,所以即使是引⽤计 数,你也应该假定读写它们是存在开销的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 auto loggingDel = [](Widget *pw) { makeLogEntry (pw); delete pw; }; std::unique_ptr< Widget, decltype (loggingDel) > upw (new Widget, loggingDel) ;std::shared_ptr<Widget> spw (new Widget, loggingDel); auto customDeleter1 = [](Widget *pw) { … }; auto customDeleter2 = [](Widget *pw) { … }; std::shared_ptr<Widget> pw1 (new Widget, customDeleter1) ;std::shared_ptr<Widget> pw2 (new Widget, customDeleter2) ;std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 }; auto pw = new Widget; std::shared_ptr<Widget> spw1 (pw, loggingDel) ; std::shared_ptr<Widget> spw2 (pw, loggingDel) ; std::shared_ptr<Widget> spw1 (new Widget, loggingDel) ;std::shared_ptr<Widget> spw2 (spw1) ; class Widget { public : …void process () ; … };void Widget::process () { … processedWidgets.emplace_back (this ); } class Widget : public std::enable_shared_from_this<Widget> {public : … void process () ; … }; void Widget::process () { … processedWidgets.emplace_back (shared_from_this ()); }
rem
std::shared_ptr 为任意共享所有权的资源⼀种⾃动垃圾回收的便捷⽅式。
较之于 std::unique_ptr , std::shared_ptr 对象通常⼤两倍,控制块会产⽣开销,需要原⼦引 ⽤计数修改操作。
默认资源销毁是通过delete,但是也⽀持⾃定义销毁器。销毁器的类型是什么对于 std::shared_ptr 的类型没有影响。
避免从原始指针变量上创建 std::shared_ptr 。
item 20 当std::shard_ptr可能悬空时使⽤std::weak_ptr ⼀个真正的智能指针应该跟踪所值 对象,在悬空时知晓,悬空(dangle)就是指针指向的对象不再存在。这就是对 std::weak_ptr 最精确的 描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 auto spw = std::make_shared <Widget>(); … std::weak_ptr<Widget> wpw (spw) ; … spw = nullptr ; if (wpw.expired ()) … std::shared_ptr<Widget> spw1 = wpw.lock (); auto spw2 = wpw.lock (); std::shared_ptr<Widget> spw3 (wpw) ; std::unique_ptr<const Widget> loadWidget (WidgetID id) ;std::shared_ptr<const Widget> fastLoadWidget (WidgetID id) { static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache; auto objPtr = cache[id].lock (); if (!objPtr) { objPtr = loadWidget (id); cache[id] = objPtr; } return objPtr; }
rem
像 std::shared_ptr 使⽤ std::weak_ptr 可能会悬空。
std::weak_ptr 的潜在使⽤场景包括:caching、observer lists、打破 std::shared_ptr 指向循 环。
item 21 优先考虑使⽤std::make_unique和std::make_shared而⾮new 本item的意⻅是,更倾向于使⽤make函数,而不是完全依赖于它们。这是因为有些情况下 它们不能或不应该被使⽤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 auto upw1 (std::make_unique<Widget>()) ; std::unique_ptr<Widget> upw2 (new Widget) ; auto spw1 (std::make_shared<Widget>()) ; std::shared_ptr<Widget> spw2 (new Widget) ; processWidget (std::shared_ptr <Widget>(new Widget), computePriority ()); processWidget (std::make_shared <Widget>(), computePriority ());std::shared_ptr<Widget> spw (new Widget) ;auto widgetDeleter = [](Widget*){...};std::unique_ptr<Widget, decltype (widgetDeleter) > upw (new Widget, widgetDeleter) ; std::shared_ptr<Widget> spw (new Widget, widgetDeleter) ;auto upv = std::make_unique<std::vector<int >>(10 , 20 ); auto spv = std::make_shared<std::vector<int >>(10 , 20 );class ReallyBigType { … };auto pBigObj = std::make_shared <ReallyBigType>(); … … … … class ReallyBigType { … }; std::shared_ptr<ReallyBigType> pBigObj (new ReallyBigType) ; … … … … processWidget ( std::shared_ptr <Widget>(new Widget, cusDel), computePriority () ); std::shared_ptr<Widget> spw (new Widget, cusDel) ; processWidget (spw, computePriority ()); processWidget (std::move (spw), computePriority ());
rem
和直接使⽤new相⽐,make函数消除了代码重复,提⾼了异常安全性。
对于 std::make_shared 和 std::allocate_shared ,⽣成的代码更小更快。
不适合使⽤make函数的情况包括需要指定⾃定义删除器和希望⽤⼤括号初始化
对于 std::shared_ptr s, make函数可能不被建议的其他情况包括
(1)有⾃定义内存管理的类和
(2)特别关注内存的系统,⾮常⼤的对象,以及 std::weak_ptr s⽐对应的 std::shared_ptr s活得 更久
item 22 当使⽤Pimpl惯⽤法,请在实现⽂件中定义特殊成员函数 会对 Pimpl (Pointer to implementation)惯⽤法很熟悉。 凭借 这样⼀种技巧,你可以将⼀个类数据成员替换成⼀个指向包含具体实现的类或结构体的指针, 并将放在主 类(primary class)的数据成员们移动到实现类去(implementation class), 而这些数据成员的访问将通过指针间接访问。 举个例⼦,假如有⼀个类 Widget 看起来如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class Widget () { public : Widget (); … private : std::string name; std::vector<double > data; Gadget g1, g2, g3; }; class Widget { public : Widget (); ~Widget (); … private : struct Impl ; Impl *pImpl; }; class Widget { public : Widget (); … private : struct Impl ; std::unique_ptr<Impl> pImpl; }; #include "widget.h" #include "gadget.h" #include <string> #include <vector> struct Widget ::Impl { std::string name; std::vector<double > data; Gadget g1,g2,g3; }; Widget::Widget () : pImpl (std::make_unique <Impl>()) {} #include "widget.h" Wdiget w; class Widget { public : Widget (); ~Widget (); … private : struct Impl ; std::unique_ptr<Impl> pImpl; }; #include "widget.h" #include "gadget.h" #include <string> #include <vector> struct Widget ::Impl { std::string name; std::vector<double > data; Gadget g1,g2,g3; } Widget::Widget () : pImpl (std::make_unique <Impl>()) {} Widget::~Widget () {} class Widget { public : Widget (); ~Widget (); Widget (Widget&& rhs) = default ; Widget& operator =(Widget&& rhs) = default ; … private : struct Impl ; std::unique_ptr<Impl> pImpl; }; class Widget { public : Widget (); ~Widget (); Widget (Widget&& rhs); Widget& operator =(Widget&& rhs); … private : struct Impl ; std::unique_ptr<Impl> pImpl; }; #include <string> … struct Widget ::Impl { … }; Widget::Widget () : pImpl (std::make_unique <Impl>()) {} Widget::~Widget () = default ; Widget::Widget (Widget&& rhs) = default ; Widget& Widget::operator =(Widget&& rhs) = default ;
rem
pImpl 惯⽤法通过减少在类实现和类使⽤者之间的编译依赖来减少编译时间。
对于 std::unique_ptr 类型的 pImpl 指针,需要在头⽂件的类⾥声明特殊的成员函数,但是在实 现⽂件⾥⾯来实现他们。即使是编译器⾃动⽣成的代码可以⼯作,也要这么做。
以上的建议只适⽤于 std::unique_ptr ,不适⽤于 std::shared_ptr 。
—– chapter 5 —– 右值引用,移动语句和完美转发 在本章的这些小节中,⾮常重要的⼀点是要牢记参数(parameter)永远是左值(lValue),即使它的类型是 ⼀个右值引⽤。⽐如,假设
item 23 理解std::move和std::forward 为了了解std::move
和std::forward
,一种有用的方式是从它们不做什么 这个角度来了解它们。std::move
不移动(move)任何东西,std::forward
也不转发(forward)任何东西。在运行时,它们不做任何事情。它们不产生任何可执行代码,一字节也没有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 template <typename T> typename remove_reference<T>::type&& move (T&& param) { using ReturnType = typename remove_reference<T>::type&&; return static_cast <ReturnType>(param); } template <typename T>decltype (auto ) move (T&& param) { using ReturnType = remove_referece_t <T>&&; return static_cast <ReturnType>(param); } class Annotation {public : explicit Annotation (const std::string text) :value (std::move(text)) { … } … private : std::string value; }; class string { public : … string (const string& rhs); string (string&& rhs); … }; void process (const Widget& lvalArg) ; void process (Widget&& rvalArg) ; template <typename T> void logAndProcess (T&& param) { auto now = std::chrono::system_clock::now (); makeLogEntry ("Calling 'process'" , now); process (std::forward<T>(param)); } Widget w; logAndProcess (w); logAndProcess (std::move (w)); class Widget {public : Widget (Widget&& rhs) : s (std::move (rhs.s)) { ++moveCtorCalls; } … private : static std::size_t moveCtorCalls; std::string s; }; class Widget {public : Widget (Widget&& rhs) : s (std::forward<std::string>(rhs.s)) { ++moveCtorCalls; } … };
rem
std::move 执⾏到右值的⽆条件的转换,但就⾃⾝而⾔,它不移动任何东西。
std::forward 只有当它的参数被绑定到⼀个右值时,才将参数转换为右值。
std::move 和 std::forward 在运⾏期什么也不做。
item 24 区分通用引用和右值已你用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void f (Widget&& param) ; Widget&& var1 = Widget (); auto && var2 = var1; template <typename T>void f (T&& param) ; template <typename MyTemplateType> void someFunc (MyTemplateType&& param) ;template <typename T>void f (std::vector<T>&& param) ; std::vector<int > v; f (v); template <typename T>void f (const T&& param) ; template <class T , class Allocator = allocator<T>> class vector{ public : void push_back (T&& x); … } std::vector<Widget> v; class vector <Widget, allocator<Widget>> {public : void push_back (Widget&& x) ; … }; template <class T , class Allocator = allocator<T>> class vector {public : template <class ... Args> void emplace_back (Args&&... args); … }; auto timeFuncInvocation = [](auto && func, auto &&... params) { start timer; std::forward<decltype (func)>(func)( std::forward<delctype (params)>(params)... ); stop timer and record elapsed time; };
牢记整个本小节——通⽤引⽤的基础——是⼀个谎⾔,uhh,⼀个“抽象”。隐藏在其底下的真相被称为”引⽤ 折叠(reference collapsing)”,小节Item 28致⼒于讨论它。而且,通用引用,传入的如果是左右值,你将对应的获得该值的左右值引用
rem
如果⼀个函数模板参数的类型为 T&& ,并且 T 需要被推导得知,或者如果⼀个对象被声明为 auto&& ,这个参数或者对象就是⼀个通⽤引⽤。
如果类型声明的形式不是标准的 type&& ,或者如果类型推导没有发⽣,那么 type&& 代表⼀个右 值引⽤。
通⽤引⽤,如果它被右值初始化,就会对应地成为右值引⽤;如果它被左值初始化,就会成为左值引⽤。
item 25 对右值引⽤使⽤std::move,对通⽤引⽤使⽤std::forward 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class Widget {public : template <typename T> void setName (T&& newName) { name = std::move (newName); } … private : std::string name; std::shared_ptr<SomeDataStructure> p; }; std::string getWidgetName () ; Widget w; auto n = getWidgetName (); w.setName (n); … class Widget {public : void setName (const std::string& newName) { name = newName; } void setName (std::string&& newName) { name = std::move (newName); } … }; template <class T , class ... Args> shared_ptr<T> make_shared (Args&&... args) ;template <class T , class ... Args> unique_ptr<T> make_unique (Args&&... args) ;Matrix operator +(Matrix&& lhs, const Matrix& rhs){ lhs += rhs; return lhs; } Matrix operator +(Matrix&& lhs, const Matrix& rhs){ lhs += rhs; return std::move (lhs); } 举个例子: Widget makeWidget () { Widget w; … return w; } Widget makeWidget () { Widget w; … return std::move (w); }
rem
在右值引⽤上使⽤ std::move ,在通⽤引⽤上使⽤ std::forward
对按值返回的函数返回值,⽆论返回右值引⽤还是通⽤引⽤,执⾏相同的操作
当局部变量就是返回值是,不要使⽤ std::move 或者 std::forward
item 26 避免在通⽤引⽤上重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 std::multiset<std::string> names; void logAndAdd (const std::string& name) { auto now = std::chrono::system_clock::now (); log (now, "logAndAdd" ); names.emplace (name); } std::string petName ("Darla" ) ;logAndAdd (petName); logAndAdd (std::string ("Persephone" )); logAndAdd ("Patty Dog" ); template <typename T>void logAndAdd (T&& name) { auto now = std::chrono::system_lock::now (); log (now, "logAndAdd" ); names.emplace (std::forward<T>(name)); } std::string petName ("Darla" ) ; logAndAdd (petName); logAndAdd (std::string ("Persephone" )); logAndAdd ("Patty Dog" ); void logAndAdd (int idx) { auto now = std::chrono::system_lock::now (); log (now, "logAndAdd" ); names.emplace (nameFromIdx (idx)); } short nameIdx;… logAndAdd (nameIdx); class Person {public : template <typename T> explicit Person (T&& n) : name(std::forward<T>(n)) { } explicit Person (int idx) ; Person (const Person& rhs); Person (Person&& rhs); … private : std::string name; }; Person p ("Nancy" ) ; auto cloneOfP (p) ; class Person {public : explicit Person (Person& n) : name(std::forward<Person&>(n)) { } explicit Person (int idx) ; Person (const Person& rhs); … }; auto cloneOfP (p) ;const Person cp ("Nancy" ) ; auto cloneOfP (cp) ;
rem
对通⽤引⽤参数的函数进⾏重载,调⽤机会会⽐你期望的多得多
完美转发构造函数是糟糕的实现,因为对于 non-const 左值不会调⽤拷⻉构造而是完美转发构造, 而且会劫持派⽣类对于基类的拷⻉和移动构造
item 27 熟悉通⽤引⽤重载的替代⽅法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 std::multiset<std::string> names; template <typename T> void logAndAdd (T&& name) { auto now = std::chrono::system_clokc::now (); log (now, "logAndAdd" ); names.emplace (std::forward<T>(name)); } template <typename T>void logAndAdd (T&& name) { logAndAddImpl ( std::forward<T>(name), std::is_instegral<typename std::remove_reference<T>::type>() ); } template <typename T> void logAndAddImpl (T&& name, std::false_type) { auto now = std::chrono::system_clock::now (); log (now, "logAndAdd" ); names.emplace (std::forward<T>(name)); } std::string nameFromIdx (int idx) ; void logAndAddImpl (int idx, std::true_type) { logAndAdd (nameFromIdx (idx)); } class Person {public : template <typename T, typename = typename std::enable_if<condition>::type> explicit Person (T&& n); … }; Person p ("Nancy" ) ; auto cloneOfP (p) ; class Person {public : template < typename T, typename = typename std::enable_if< !std::is_same< Person, typename std::decay<T>::type >::value >::type > explicit Person (T&& n); … }; class SpecialPerson : public Person {public : SpecialPerson (const SpecialPerson& rhs) : Person (rhs) { … } SpecialPerson (SpecialPerson&& rhs) : Person (std::move (rhs)) { … } … }; class Person { public : template < typename T, typename = std::enable_if_t < !std::is_base_of<Person, std::decay_t <T> >::value > > explicit Person (T&& n); … }; class Person {public : template < typename T, typename = std::enable_if_t < !std::is_base_of<Person, std::decay_t <T>>::value && !std::is_integral<std::remove_reference_t <T>>::value > > explicit Person (T&& n) : name (std::forward<T>(n)) { … } explicit Person (int idx) : name (nameFromIdx (idx)) { … } … private : std::string name; }; Person p (u"Konrad Zuse" ) ; class Person {public : template < typename T, typename = std::enable_if_t < !std::is_base_of<Person, std::decay_t <T>>::value && !std::is_integral<std::remove_reference_t <T>>::value > > explicit Person (T&& n) : name (std::forward<T>(n)) { static_assert ( std::is_constructible<std::string, T>::value, "Parameter n can't be used to construct a std::string" ); … } … };
rem
通⽤引⽤和重载的组合替代⽅案包括使⽤不同的函数名,通过const左值引⽤传参,按值传递参 数,使⽤tag dispatch
通过 std::enable_if 约束模板,允许组合通⽤引⽤和重载使⽤, std::enable_if 可以控制编译 器哪种条件才使⽤通⽤引⽤的实例
通⽤引⽤参数通常具有⾼效率的优势,但是可⽤性就值得斟酌
item 28 理解引⽤折叠 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 int x;auto & & rx = x; template <typename T>void func (T&& param) ; func (w); void func (Widget& && param) ;void func (Widget& param) ;template <typename T>void f (T&& fParam) { … someFunc (std::forward<T>(fParam)); } template <typename T> T&& forward (typename remove_reference<T>::type& param) { return static_cast <T&&>(param); } Widget& && forward (typename remove_reference<Widget&>::type& param) { return static_cast <Widget& &&>(param); }Widget& && forward (Widget& param) { return static_cast <Widget& &&>(param); }Widget& forward (Widget& param) { return static_cast <Widget&>(param); }Widget&& forward (typename remove_reference<Widget>::type& param) { return static_cast <Widget&&>(param); }Widget&& forward (Widget& param) { return static_cast <Widget&&>(param); }template <typename T> T&& forward (remove_reference_t <T>& param) { return static_cast <T&&>(param); } Widget widgetFactory () ; Widget w; func (w); func (widgetFactory ()); auto && w1 = w;auto && w2 = widgetFactory ();template <typename T>class Widget {public : typedef T&& RvalueRefToT; … }; typedef int & && RvalueRefToT;typedef int & RvalueRefToT;
rem
引⽤折叠发⽣在四种情况:模板实例化;auto类型推导;typedef的创建和别名声明;decltype
当编译器⽣成了引⽤的引⽤时,结果通过引⽤折叠就是单个引⽤。有左值引⽤就是左值引⽤,否则 就是右值引⽤
通⽤引⽤就是通过类型推导区分左值还是右值,并且引⽤折叠出现的右值引⽤
item 29 理智看待std::move Assume that move operations are not present, not cheap, and not used. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 std::vector<Widget> vm1; auto vm2 = std::move (vm1); std::array<Widget, 10000> aw1; auto aw2 = std::move (aw1);
rem
Assume that move operations are not present, not cheap, and not used.
完全了解的代码可以忽略本Item
item 30 熟悉完美转发的失败case 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 template <typename ... Ts>void fwd (Ts&&... params) { f (std::forward<Ts>(params)...); } f ( expression ); fwd ( expression ); void f (const std::vector<int >& v) ;f ({1 ,2 ,3 }); fwd ({1 ,2 ,3 }); auto il = {1 ,2 ,3 }; fwd (il); class Widget {public : static const std::size_t MinVals = 28 ; … }; … std::vector<int > widgetData; widgetData.reserve (Widget::MinVals); void f (std::size_t val) ; f (Widget::MinVals); fwd (Widget::MinVals); const std::size_t Widget::MinVals; void f (int (*pf)(int )) ; void f (int pf(int )) ; int processVal (int value) ; int processVal (int value, int priority) ;f (processVal); fwd (processVal); template <typename T> T workOnVal (T param) { ... } fwd (workOnVal); using ProcessFuncType = int (*)(int ); ProcessFuncType processValPtr = processVal; fwd (processValPtr); fwd (static_cast <ProcessFuncType>(workOnVal)); struct IPv4Header { std::uint32_t version:4 , IHL:4 , DSCP:6 , ECN:2 , totalLength:16 ; }; void f (std::size_t sz) ; IPv4Header h; … f (h.totalLength); fwd (h.totalLength); auto length = static_cast <std::uint16_t >(h.totalLength); fwd (length);
rem
完美转发会失败当模板类型推导失败或者推导类型错误
导致完美转发失败的类型有
braced initializers,
作为空指针的0或者NULL,
只声明的整型static const数据成员,
模板和重载的函数名
位域
—— chapter 6 —— lambda 表达式
lambda 通常被用来创建闭包,该闭包仅用作函数的实参。上面对std::find_if
的调用就是这种情况。然而,闭包通常可以拷贝,所以可能有多个闭包对应于一个lambda 。比如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 { int x; … auto c1 = [x](int y) { return x * y > 55 ; }; auto c2 = c1; auto c3 = c2; … }
c1
,c2
,c3
都是lambda 产生的闭包的副本。
非正式的讲,模糊lambda ,闭包和闭包类之间的界限是可以接受的。但是,在随后的Item中,区分什么存在于编译期(lambdas 和闭包类),什么存在于运行时(闭包)以及它们之间的相互关系是重要的。
item 31 避免使⽤默认捕获模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 using FilterContainer = std::vector<std::function<bool (int )>>; FilterContainer filters; filters.emplace_back ( [](int value) { return value % 5 == 0 ; } ); void addDivisorFilter () { auto calc1 = computeSomeValue1 (); auto calc2 = computeSomeValue2 (); auto divisor = computeDivisor (calc1, calc2); filters.emplace_back ( [&](int value) { return value % divisor == 0 ; } ); } filters.emplace_back ( [&divisor](int value) { return value % divisor == 0 ; } ); filters.emplace_back ( [=](int value) { return value % divisor == 0 ; } ); class Widget {public : … void addFilter () const ; private : int divisor; }; void Widget::addFilter () const { filters.emplace_back ( [=](int value) { return value % divisor == 0 ; } ); } void Widget::addFilter () const { filters.emplace_back ( [](int value) { return value % divisor == 0 ; } ); } void Widget::addFilter () const { filters.emplace_back ( [divisor](int value) { return value % divisor == 0 ; } ); } void Widget::addFilter () const { filters.emplace_back ( [=](int value) { return value % divisor == 0 ; } ); } void Widget::addFilter () const { auto currentObjectPtr = this ; filters.emplace_back ( [currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0 ; } ); } using FilterContainer = std::vector<std::function<bool (int )>>; FilterContainer filters; void doSomeWork () { auto pw = std::make_unique <Widget>(); pw->addFilter (); … } void Widget::addFilter () const { auto divisorCopy = divisor; filters.emplace_back ( [divisorCopy](int value) { return value % divisorCopy == 0 ; } ); } void Widget::addFilter () const { auto divisorCopy = divisor; filters.emplace_back ( [=](int value) { return value % divisorCopy == 0 ; } ); } void Widget::addFilter () const { filters.emplace_back ( [divisor = divisor](int value) { return value % divisor == 0 ; } ); } void addDivisorFilter () { static auto calc1 = computeSomeValue1 (); static auto calc2 = computeSomeValue2 (); static auto divisor = computeDivisor (calc1, calc2); filters.emplace_back ( [=](int value) { return value % divisor == 0 ; } ); ++divisor; }
rem
默认的按引⽤捕获可能会导致悬空引⽤;
默认的按值引⽤对于悬空指针很敏感(尤其是this指针),并且它会误导⼈产⽣lambda是独⽴的想 法;
item 32 使⽤初始化捕(⼴义lambda捕获)获来移动对象到闭包中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 class Widget { public : … bool isValidated () const ; bool isProcessed () const ; bool isArchived () const ; private : … }; auto pw = std::make_unique <Widget>(); … auto func = [pw = std::move (pw)] { return pw->isValidated () && pw->isArchived (); }; auto func = [pw = std::make_unique <Widget>()] { return pw->isValidated () && pw->isArchived (); }; class IsValAndArch { public : using DataType = std::unique_ptr<Widget>; explicit IsValAndArch (DataType&& ptr) : pw(std::move(ptr)) { } bool operator () () const { return pw->isValidated () && pw->isArchived (); } private : DataType pw; }; auto func = IsValAndArch (std::make_unique <Widget>());std::vector<double > data; … auto func = [data = std::move (data)] { }; auto func = std::bind ( [](const std::vector<double >& data) { }, std::move (data) ); auto func = std::bind ( [](std::vector<double >& data) mutable { }, std::move (data) ); auto func = [pw = std::make_unique <Widget>()] { return pw->isValidated () auto func = std::bind ( [](const std::unique_ptr<Widget>& pw) { return pw->isValidated () && pw->isArchived (); }, std::make_unique <Widget>() );
rem
使⽤C ++14的初始化捕获将对象移动到闭包中。
在C ++11中,通过⼿写类或 std::bind 的⽅式来模拟初始化捕获。
item 33 对于std::forward的auto&&形参使⽤decltype 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 auto f = [](auto x){ return func (normalize (x)); };class SomeCompilerGeneratedClassName {public : template <typename T> auto operator () (T x) const { return func (normalize (x)); } … }; auto f = [](auto && x) { return func (normalize (std::forward<???>(x))); }; template <typename T> T&& forward (remove_reference_t <T>& param) { return static_cast <T&&>(param); } Widget&& forward (Widget& param) { return static_cast <Widget&&>(param); } Widget&& && forward (Widget& param) { return static_cast <Widget&& &&>(param); } Widget&& forward (Widget& param) { return static_cast <Widget&&>(param); } auto f = [](auto && param) { return func (normalize (std::forward<decltype (param)>(param))); }; auto f = [](auto &&... params) { return func (normalize (std::forward<decltype (params)>(params)...)); };
rem
对 auto&& 参数使⽤ decltype 来( std::forward )转发参数;
item 34 考虑lambda表达式而⾮std::bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 using Time = std::chrono::steady_clock::time_point;enum class Sound { Beep, Siren, Whistle };using Duration = std::chrono::steady_clock::duration;void setAlarm (Time t, Sound s, Duration d) ;auto setSoundL = [](Sound s) { using namespace std::chrono; setAlarm (steady_clock::now () + hours (1 ), s, seconds (30 )); }; auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; setAlarm (steady_clock::now () + 1 h, s, 30 s); }; using namespace std::chrono; using namespace std::literals;using namespace std::placeholders; auto setSoundB = std::bind (setAlarm, steady_clock::now () + 1 h, _1, 30 s); auto setSoundB = std::bind (setAlarm, std::bind (std::plus<>(), steady_clock::now (), 1 h), _1, 30 s); auto setSoundB = std::bind (setAlarm, std::bind (std::plus<>(), steady_clock::now (), 1 h), _1, 30 s); using namespace std::chrono; using namespace std::placeholders;auto setSoundB = std::bind (setAlarm, std::bind (std::plus <steady_clock::time_point>(), steady_clock::now (), hours (1 )), _1, seconds (30 )); enum class Volume { Normal, Loud, LoudPlusPlus }; void setAlarm (Time t, Sound s, Duration d, Volume v) ;auto setSoundL = [](Sound s) { using namespace std::chrono; setAlarm (steady_clock::now () + 1 h, s, 30 s); }; auto setSoundB = std::bind (setAlarm, std::bind (std::plus<>(), steady_clock::now (), 1 h), _1, 30 s); using SetAlarm3ParamType = void (*)(Time t, Sound s, Duration d);auto setSoundB = std::bind (static_cast <SetAlarm3ParamType>(setAlarm), std::bind (std::plus<>(), steady_clock::now (), 1 h), _1, 30 s); setSoundL (Sound::Siren); setSoundB (Sound::Siren); auto betweenL = [lowVal, highVal] (const auto & val) { return lowVal <= val && val <= highVal; }; using namespace std::placeholders; auto betweenB = std::bind (std::logical_and<>(), std::bind (std::less_equal<>(), lowVal, _1), std::bind (std::less_equal<>(), _1, highVal)); enum class CompLevel { Low, Normal, High }; Widget compress (const Widget& w, CompLevel lev) ;Widget w; using namespace std::placeholders;auto compressRateB = std::bind (compress, w, _1);auto compressRateL = [w](CompLevel lev) { return compress (w, lev); }; compressRateL (CompLevel::High); compressRateB (CompLevel::High); class PolyWidget {public : template <typename T> void operator () (const T& param) ; … }; PolyWidget pw; auto boundPW = std::bind (pw, _1);boundPW (1930 ); boundPW (nullptr ); boundPW ("Rosebud" ); auto boundPW = [pw](const auto & param) { pw (param); };
rem
与使⽤ std::bind 相⽐,Lambda更易读,更具表达⼒并且可能更⾼效。
只有在C++11中, std::bind 可能对实现移动捕获或使⽤模板化函数调⽤运算符来绑定对象时会很 有⽤。
——- chapter 7 ——- 并发API C++11的伟大成功之一是将并发整合到语言和库中。熟悉其他线程API(比如pthreads或者Windows threads)的开发者有时可能会对C++提供的斯巴达式(译者注:应该是简陋和严谨的意思)功能集感到惊讶,这是因为C++对于并发的大量支持是在对编译器作者约束的层面。开发者首次通过标准库可以写出跨平台的多线程程序。这为构建表达库奠定了坚实的基础,标准库并发组件(任务tasks ,期望futures ,线程threads ,互斥mutexes ,条件变量condition variables ,原子对象atomic objects 等)仅仅是成为并发软件开发者丰富工具集的基础。
记住标准库有两个future 的模板:std::future
和std::shared_future
。在许多情况下,区别不重要,所以我们经常简单的混于一谈为futures 。
item 35 优先基于任务编程而不是基于线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int doAsyncWork () ; std::thread t (doAsyncWork) ;auto fut = std::async (doAsyncWork); int doAsyncWork () noexcept ; std::thread t (doAsyncWork) ; auto fut = std::async (doAsyncWork);
rem
std::thread API不能直接访问异步执⾏的结果,如果执⾏函数有异常抛出,代码会终⽌执⾏
基于线程的编程⽅式关于解决资源超限,负载均衡的⽅案移植性不佳
基于任务的编程⽅式 std::async 会默认解决上⾯两条问题
item 36 必须异步执行,就指定std::launch::async 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 auto fut1 = std::async (f); auto fut2 = std::async (std::launch::async | std::launch::deferred, f); auto fut = std::async (f); auto fut = std::async (f); using namespace std::literals; void f () { std::this_thread::sleep_for (1 s); } auto fut = std::async (f); while (fut.wait_for (100 ms) != std::future_status::ready) { … } auto fut = std::async (f); if (fut.wait_for (0 s) == std::future_status::deferred) { … } else { while (fut.wait_for (100 ms) != std::future_status::ready) { … } … } auto fut = std::async (std::launch::async, f); 事实上,具有类似 std::async ⾏为的函数,但是会⾃动使⽤ std::launch::async 作为启动策略的⼯ 具也是很容易编写的,C++11 \14 版本如下: template <typename F, typename ... Ts> inline std::future<typename std::result_of<F (Ts...)>::type> reallyAsync (F&& f, Ts&&... params) { return std::async (std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...); } template <typename F, typename ... Ts>inline auto reallyAsync (F&& f, Ts&&... params){ return std::async (std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...); }
rem
std::async 的默认启动策略是异步或者同步的
灵活性导致访问thread_locals的不确定性,隐含了task可能不会被执⾏的意思,会影响程序基于 wait 的超时逻辑
只有确实异步时才指定 std::launch::async
item 37 确保std::threads在所有路径都不可join(也就是确保都被join过了) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 constexpr auto tenMillion = 10000000 ; bool doWork (std::function<bool (int )> filter, int maxVal = tenMillion) { std::vector<int > goodVals; std::thread t ([&filter, maxVal, &goodVals] { for (auto i = 0 ; i <= maxVal; ++i) { if (filter(i)) goodVals.push_back(i); } }) ; auto nh = t.native_handle (); … if (conditionsAreSatisfied ()) { t.join (); performComputation (goodVals); return true ; } return false ; } class ThreadRAII {public : enum class DtorAction { join, detach }; ThreadRAII (std::thread&& t, DtorAction a) : action (a), t (std::move (t)) {} ~ThreadRAII () { if (t.joinable ()) { if (action == DtorAction::join) { t.join (); } else { t.detach (); } } } ThreadRAII (ThreadRAII&&) = default ; ThreadRAII& operator =(ThreadRAII&&) = default ; std::thread& get () { return t; } private : DtorAction action; std::thread t; }; if (t.joinable ()) { if (action == DtorAction::join) { t.join (); } else { t.detach (); } }
rem
在所有路径上保证 thread 最终是unjoinable
析构时 join 会导致难以调试的性能异常问题
析构时 detach 会导致难以调试的未定义⾏为
声明类数据成员时,最后声明 std::thread 类型成员(因为最终声明thread可以保证这个变量最后初始化)
item 38 明白不同线程句柄的析构⾏为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream> #include <future> #include <thread> int main () { std::packaged_task<int () > task ([]{ return 7 ; }) ; std::future<int > f1 = task.get_future (); std::thread t (std::move(task)) ; std::future<int > f2 = std::async (std::launch::async, []{ return 8 ; }); std::promise<int > p; std::future<int > f3 = p.get_future (); std::thread ( [&p]{ p.set_value_at_thread_exit (9 ); }).detach (); std::cout << "Waiting..." << std::flush; f1.wait (); f2.wait (); f3.wait (); std::cout << "Done!\nResults are: " << f1.get () << ' ' << f2.get () << ' ' << f3.get () << '\n' ; t.join (); } { std::packaged_task<int () > pt (calcValue) ; auto fut = pt.get_future (); std::thread t (std::move(pt)) ; ... }
rem
future 的正常析构⾏为就是销毁 future 本⾝的成员数据
最后⼀个引⽤ std::async 创建共享状态的 future 析构函数会在任务结束前block
item 39 对于一次性通讯使用返回void的futures 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 std::condition_variable cv; std::mutex m; ... cv.notify_one (); … { std::unique_lock<std::mutex> lk (m) ; cv.wait (lk); … } … std::atomic<bool > flag (false ) ; ... flag = true ; ... while (!flag); ... std::condition_variable cv; std::mutex m; bool flag (false ) ; … { std::lock_guard<std::mutex> g (m) ; flag = true ; } cv.notify_one (); … { std::unique_lock<std::mutex> lk (m) ; cv.wait (lk, [] { return flag; }); … } … std::promise<void > p; … p.set_value (); … p.get_future ().wait (); … std::promise<void > p; void react () ; void detect () { ThreadRAII tr ( std::thread([] { p.get_future().wait(); react(); }), ThreadRAII::DtorAction::join ) ; … p.set_value (); … } std::promise<void > p; void detect () { auto sf = p.get_future ().share (); std::vector<std::thread> vt; for (int i = 0 ; i < threadsToRun; ++i) { vt.emplace_back ([sf]{ sf.wait (); react (); }); } … p.set_value (); … for (auto & t : vt) { t.join (); } }
rem
对于简单的事件通信,条件变量需要⼀个多余的互斥锁,对检测和反应任务的相对进度有约束,并 且需要反应任务来验证事件是否已发⽣
基于flag的设计避免的上⼀条的问题,但是不是真正的挂起反应任务
组合条件变量和flag使⽤,上⾯的问题都解决了,但是逻辑不让⼈愉快
使⽤ std::promise和future 的⽅案,要考虑堆内存的分配和销毁开销,同时有只能使⽤⼀次通信 的限制
item 40 当需要并发时使⽤ std::atomic ,特定内存才使⽤ volatile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 std::atomic<int > ai (0 ) ; ai = 10 ; std::cout << ai; ++ai; --ai; volatile int vi (0 ) ; vi = 10 ; std::cout << vi; ++vi; --vi; std::atomic<bool > valVailable (false ) ; auto imptValue = coputeImportantValue (); valAvailable = true ; a = b; x = y; x = y; a = b; auto impatValue = computeImportantValue (); valVailable = true ; volatile bool valAvaliable (false ) ; auto imptValue = computeImportantValue (); valAvailable = true ; auto y = x; y = x; volatile int x;auto y = x; y = x; x = 10 ; x = 20 ; std::atomic<int > x; auto y = x; y = x; x = 10 ; x = 20 ; auto y = x; x = 20 ; auto y = x; y = x; std::atomic<int > y (x.load()) ; y.store (x.load ()); register = x.load (); std::atomic<int > y (register ) ; y.store (register ); volatile std::atomic<int > vai;
rem
std::atomic是⽤在不使⽤锁,来使变量被多个线程访问。是⽤来编写并发程序的
volatile 是⽤在特殊内存的场景中,避免被编译器优化内存。
——– chapter 8 ——– 微调 item 41 如果参数可拷⻉并且移动操作开销很低,总是考虑 直接按值传递 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 class Widget {public : template <typename T> void addName (T&& newName) { names.push_back (std::forward<T>(newName)); } … }; class Widget {public : void addName (std::string newName) { names.push_back (std::move (newName)); } … } Widget w; … std::string name ("Bart" ) ;w.addName (name); … w.addName (name + "Jenne" ); class Widget { public : void addName (const std::string& newName) { names.push_back (newName); } void addName (std::string&& newName) { names.push_back (std::move (newName)); } … private : std::vector<std::string> names; }; class Widget { public : template <typename T> void addName (T&& newName) { names.push_back (std::forward<T>(newName)); } … }; class Widget { public : void addName (std::string newName) { names.push_back (std::move (newName)); } … }; class Widget { public : … void setPtr (std::unique_ptr<std::string>&& ptr) { p = std::move (ptr); } private : std::unique_ptr<std::string> p; }; Widget w; … w.setPtr (std::make_unique <std::string>("Modern C++" )); class Widget { public : … void setPtr (std::unique_ptr<std::string> ptr) { p = std::move (ptr); } … };
rem
对于可复制,移动开销低,而且⽆条件复制的参数,按值传递效率基本与按引⽤传递效率⼀致,而 且易于实现,⽣成更少的⽬标代码
通过构造函数拷⻉参数可能⽐通过赋值拷⻉开销⼤的多 按值传递会引起切⽚问题,所说不适合基类类型的参数
item 42 考虑使⽤emplacement代替insertion 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 std::vector<std::string> vs; vs.push_back ("xyzzy" ); template <class T , class Allocator = allocator<T>> class vector {public : … void push_back (const T& x); void push_back (T&& x) ; … }; vs.push_back (std::string ("xyzzy" )); vs.emplace_back ("xyzzy" ); vs.emplace_back (50 , 'x' ); std::string queenOfDisco ("Donna Summer" ) ;vs.push_back (queenOfDisco); vs.emplace_back (queenOfDisco); std::list<std::shared_ptr<Widget>> ptrs; void killWidget (Widget* pWidget) ;ptrs.push_back (std::shared_ptr <Widget>(new Widget, killWidget)); ptrs.push_back ({new Widget, killWidget}); ptrs.emplace_back (new Widget, killWidget); regexes.emplace_back (nullptr ); regexes.push_back (nullptr ); std::regex r = nullptr ; regexes.push_back (nullptr ); std::regex r1 = nullptr ; std::regex r2 (nullptr ) ; regexes.emplace_back (nullptr ); regexes.push_back (nullptr );
rem
原则上,emplacement函数有时会⽐insertion函数⾼效,并且不会更差
实际上,当执⾏如下操作时,emplacement函数更快
值被构造到容器中,而不是直接赋值
传⼊的类型与容器类型不⼀致
容器不拒绝已经存在的重复值
emplacement函数可能执⾏insertion函数拒绝的显⽰构造