资讯详情

Effective Modern C++42招独家技巧助你改善C++11和C++14的高效用法笔记

这篇博文来自博主fengbingchun,如果您想查阅更多内容,可以移动到fengbingchun。

转载自:https://blog.csdn.net/fengbingchun/article/details/104136592侵删

Scott Meyers大师Effective三部曲:Effective C 、More Effective C 、Effective STL,这三本书出版多年,后来又出版了Effective Modern C 。

Effective C 的笔记见:https://blog.csdn.net/fengbingchun/article/details/102761542

More Effective C 的笔记见:https://blog.csdn.net/fengbingchun/article/details/102990753

Effective STL的笔记见:https://blog.csdn.net/fengbingchun/article/details/103223914

这里是Effective Modern C 的笔记:

注:(1).下面的测试代码可以Windows也可以在下面执行Linux执行。(2).就我个人而言,我觉得中文版的一些内容没有直接阅读英文版那么透彻,所以下面的一些中文也给出了相应的英文。

C 移动语义可能是最广泛接受的特征,移动语义的基础是区分左表达式和右表达式。因为一个对象是右值,这意味着它可以实现移动语义,而左值通常不是。从概念上讲(实际上并不总是成立的),右值对应函数返回的临时对象,左值对应可指相关对象,无论是名称、指针还是左值引用。

有一种实用的方法来识别表达式是否是左值,那就是检查表达式的地址是否可以获得。若可获得,则表达式基本上可以判断为左值。若无法获得,则通常为右值。这种方法很有启发性,因为它让你记住表达式的类型(type)与它是左还是右无关。换句话说,给定一种类型T,既有T型的左值,也有T型的右值。

在函数调用中,调用方的表达式称为函数的实参。实参的用途是初始函数的形参。实参和形参有很大的区别,因为形参是左值,用作初始化基础的实参可能是右值或左值。

//template<typename T> //void f(ParamType param);  template<typename T> void f(T& param) {} // param是个引用  template<typename T> void f2(T* param) {} // param现在是指针  template<typename T> void f3(T&& param) {} // param现在是万能引用  template<typename T> void f4(T param) {} // param现在是按值传递  // 数组尺寸以编译期常量的形式返回(数组形未命名,因为我们只关系元素的数量) template<typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept // 声明函数为constexpr,它的返回值可以在编译期使用。这样就可以了 {        // 当声明数组时,它的尺寸与另一个数组相同,而后者的尺寸从花括号开始(braced initializer)计算得出  return N; }  void someFunc(int, double) {} // someFunc是函数,其类型为void(int, double)  int test_item_1() {  //f(expr); // 某表达式调用f  // 编译器将通过编译期expr推导两个型别:一个是T的型别,另一个是ParamType的型别,这两种类型往往不同   int x = 27; // x的型别是int  const int cx = x; // cx的型别是const int  const int& rx = x; // rx是x型const int的引用   f(x); // T的型别是int, param的型别是int&  f(cx); // T的型别是const int, param的型别是const int&  f(rx); // T的型别是const int, param的型别是const int&, 注意:即使rx引用型,T由于没有被推导成引用,rx的引用性(reference-ness)类别推导过程中会被忽略   const int* px = &x; // px is ptr to x as a const int  f2(&x); // T is int, param's type is int*  f2(px); // T is const int, param's type is const int*   f3(x); // x is lvalue, so T is int&, param's type is also int&  f3(cx); // cx is lvalue, so T is const int&, param's type is also const int&  f3(rx); // rx is lvalue, so T is const int&, param's type is also const int&  f3(27); // 27 is rvalue, so T is int, param's type is therefore int&&   // param是完全独立的cx和rx存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象——是存在的对象—是存在的对象—是存在的对象——是存在的对象—是存在的对象—是存在的对象——是存在的对象—是存在的对象—是存在的对象——是存在的对象—是存在的对象—是存在的对象—是存在的对象——是存在的对象—是存在的对象—是存在的对象—是存在的对象——是存在的对象——是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—是存在的对象—存在的对象—存在的对象—是存在的存在的对象—cx和rx的一个副本  f4(x); // T's and param's types are both int  f4(cx); // T's and param's types are again both int  f4(rx); // T's and param's types are still both int   const char* const ptr = "Fun with pointers"; // ptr is const pointer to const object  f4(ptr); // pass arg of type const char* const   const char name[] = "J. P. Briggs"; // name's type is const char[13]  const char* ptrToName = name; // array decays to pointer   f4(name); // name is array, but T deduced as const char*  f(name); // pass array to f, T类别推导的结果是const char[13], F的形参类型(数组的引用)被推导为const char (&)[13]   int keyVals[] = {1, 3, 7, 9, 11, 22, 35};  fprintf(stdout, "array length: %d\n", arraySize(keyVals)); // 7  int mappedVals[arraySize(keyVals)]; // mappedVals指定相同  std::array<int, arraySize(keyVals)> mappedVals2; // mappedVals也指定为7个元素   f4(someFunc); // param被推导为函数指针(ptr-to-func),具体型别是void (*)(int, double)  f(someFunc); // param被推导为函数引用(ref-to-func), 具体型别是void (&)(int, double)   return 0; }

T类别推导结果不仅取决于expr依赖于类型ParamType形式。讨论应分为三种情况:

(1).ParamType有指针或引用类型,但不是万能引用(universal reference):若expr有引用型,首先忽略引用部分;然后对expr的型别和ParamTypeT型的类型与执行模式相匹配。

(2).ParamType是万能引用(universal reference):此类形参的明方式类似右值引用(即在函数模板中持有型别形参T时,万能引用的声明型别写作T&&),但是当传入的实参是左值时,其表现会有所不同。如果expr是个左值,T和ParamType都会被推导为左值引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。如果expr是个右值,则应用”常规”(即情况1中的)规则。当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用是从来不会做这样的区分的。

(3).ParamType既非指针也非引用:当ParamType既非指针也非引用时,我们面对的就是所谓按值传递了。一如之前,若expr具有引用型别,则忽略其引用部分。忽略expr的引用性之后,若expr是个const对象,也忽略之。若其是个volatile对象,同样忽略之(volatile对象不常用,它们一般仅用于实现设备驱动程序)。

数组实参:数组型别(array type)有别于指针型别,尽管有时它们看起来可以互换。形成这种假象的主要原因是,在很多语境下,数组会退化成指涉到其首元素的指针。可以利用声明数组引用这一能力创造出一个模板,用来推导出数组含有的元素个数。

函数实参:数组并非C++中唯一可以退化为指针之物。函数型别也同样会退化成函数指针,并且我们针对数组型别推导的一切讨论都适用于函数及其向函数指针的退化。

//template<typename T>
//void f(ParamType param);
 
void someFunc2(int, double) {} // someFunc是个函数,其型别为void(int, double)
 
/*auto createInitlist()
{
    return {1, 2, 3}; // error: can't deduce type for {1, 2, 3}
}*/
 
int test_item_2()
{
    //f(expr); // 已某表达式调用f
    // 当某变量采用auto来声明时,auto就扮演了模板中的T这个角色,而变量的型别饰词则扮演的是ParamType的角色
    auto x = 27; // x的型别饰词(type specifier)就是auto自身, x既非指针也非引用
    const auto cx = x; // 型别饰词成了const auto, cx既非指针也非引用
    const auto& rx = x; // 型别饰词又成了const auto&, rx是个引用,但不是万能引用
 
    auto&& uref1 = x; // x的型别是int,且是左值,所以uref1的型别是int&
    auto&& uref2 = cx; // cx的型别是const int, 且是左值,所以uref2的型别是const int&
    auto&& uref3 = 27; // 27的型别是int,且是右值,所以uref3的型别是int&&
 
    const char name[] = "R. N. Briggs"; // name的型别是const char[13]
    auto arr1 = name; // arr1's type is const char*
    auto& arr2 = name; // arr2's type is const char (&)[13]
 
    auto func1 = someFunc2; // func1's type is void(*)(int, double)
    auto& func2 = someFunc2; // func2's type is void(&)(int, double)
 
    // 若要声明一个int,并将其初始化为值27,C++98中有两种可选语法
    int x1 = 27;
    int x2(27);
    // 而C++11为了支持统一初始化(uniform initialization),增加了下面的语法选项
    int x3 = {27};
    int x4{27};
 
    auto x1_1 = 27; // type is int, value is 27
    auto x2_1(27); // type is int, value is 27
    auto x3_1 = {27}; // type is std::initializer_list<int>, value is {27}
    auto x4_1{27}; // type is std::initializer_list<int>, value is {27}
    //auto x5_1 = {1, 2, 3.0}; // error, can't deduce T for std::initializer_list<T> 
 
    std::vector<int> v;
    auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
    //resetV({1, 2, 3}); // error, can't deduce type for {1, 2, 3}
 
    return 0;
}

除了一个奇妙的例外情况以外,auto型别推导就是模板型别推导。在采用auto进行变量声明中,型别饰词取代了ParamType,所以也存在三种情况:(1).型别饰词是指针或引用,但不是万能引用(universal reference)。(2).型别饰词是万能引用。(3).型别饰词既非指针也非引用。

当用于auto声明变量的初始化表达式是使用大括号括起时,推导所得的型别就属于std::initializer_list。这么一来,如果型别推导失败(例如,大括号里的值型别不一),则代码就通不过编译。对于大括号初始化表达式的处理方式是auto型别推导和模板型别推导的唯一不同之处。当采用auto声明的变量使用大括号初始化表达式进行初始化时,推导所得的型别是std::initializer_list的一个实例型别,但模板型别却不会。

C++14允许使用auto来说明函数返回值需要推导,而且C++14中的lambda式也会在形参声明中用到auto。然而,这些auto用法是在使用模板型别推导而非auto型别推导。所以,带有auto返回值的函数若要返回一个大括号括起来的初始化表达式,是通不过编译的。同样地,用auto来指定C++14中lambda式的形参型别时,也不能使用大括号括起的初始化表达式。

auto更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/51834927

class Widget3 {};
bool f5(const Widget3& w) { return true; } // decltype(w) is const Widget3&; decltype(f5) is bool(const Widgeet3&)
 
template<typename Container, typename Index>
// 这里的auto只为说明这里使用了C++11中的返回值型别尾序语法(trailing return type syntax),即该函数的返回值型别将在形参列表之后(在"->"之后)
// 尾序返回值的好处在于,在指定返回值型别时可以使用函数形参
//auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) // C++11
decltype(auto) authAndAccess(Container&& c, Index i) // C++14, c is now a universal reference
{
    return std::forward<Container>(c)[i];
}
 
struct Point {
    int x, y; // decltype(Point::x) is int; decltype(Point::y) is int
};
 
decltype(auto) ff3_1()
{
    int x = 0;
    return x; // decltype(x)是int,所以ff3_1返回的是int
    //return (x); // decltype((x))是int&,所以ff3_1返回的是int&
}
 
int test_item_3()
{
    const int i = 0; // decltype(i) is const int
    Widget3 w; // decltype(w) is Widget3
    if (f5(w)) {} // decltype(f5(w)) is bool
    std::vector<int> v; // decltype(v) is vector<int>
 
    const Widget3& cw = w;
    auto myWidget31 = cw; // auto型别推导:myWidget31的型别是Widget3
    decltype(auto) myWidget32 = cw; // decltype型别推导:myWidget32的型别是const Widget3&
 
    return 0;
}

对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别。与模板和auto的型别推导过程相反,decltype一般只会鹦鹉学舌,返回给定的名字或表达式的确切型别而已。

C++11中,decltype的主要用途大概就在于声明那些返回值型别依赖于形参型别的函数模板。

C++11允许对单表达式的lambda式的返回值型别实施推导,而C++14则将这个允许范围扩张到了一切lambda式和一切函数,包括那些多表达式。

C++14中的decltype(auto)并不限于在函数返回值型别处使用。在变量声明的场合上,若你也想在初始化表达式处应用decltype型别推导规则,也可以照样便宜行事。

容器的传递方式是对非常量的左值引用(lvalue-reference-to-non-const)。

decltype更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/52504519

int test_item_4()
{
    const int theAnswer = 42;
    auto x = theAnswer; // int
    auto y = &theAnswer; // const int*
    fprintf(stdout, "%s, %s\n", typeid(x).name(), typeid(y).name());
 
    return 0;
}

IDE编辑器:IDE中的代码编辑器通常会在你将鼠标指针悬停至某个程序实体,如变量、形参、函数等时,显示出该实体的型别。IDE显示的型别信息不可靠。

编译器诊断信息:想要让编译器显示其推导出的型别,一条有效的途径是使用该型别导致某些编译错误。而报告错误的消息几乎肯定会提及导致该错误的型别。

运行时输出:使用printf来显示型别信息,这种方法只有到了运行期才能使用,却可以对于型别输出的格式提供完全的控制。std::type_info::name并不可靠。

class Widget5 {};
bool operator<(const Widget5& lhs, const Widget5& rhs) { return true; }
 
int test_item_5()
{
    int x1; // potentially uninitialized
    //auto x2; // error, initializer required
    auto x3 = 0; // fine, x's value is well-defined
 
    auto derefUPLess = [](const std::unique_ptr<Widget5>& p1, const std::unique_ptr<Widget5>& p2) { return *p1 < *p2; }; // comparison func. for Widget5 pointed to by std::unique_ptrs
    auto derefLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; }; // C++14 comparison function for values pointed to by anything pointer-like
 
    // bool(const std::unique_ptr<Widget5>&, const std::unique_ptr<Widget5>&) // C++11 signature for std::unique_ptr<Widget5> comparison function
    std::function<bool(const std::unique_ptr<Widget5>&, const std::unique_ptr<Widget5>&)> func;
 
    // 在C++11中,不用auto也可以声明derefUPLess
    std::function<bool(const std::unique_ptr<Widget5>&, const std::unique_ptr<Widget5>&)> derefUPLess2 = [](const std::unique_ptr<Widget5>& p1, const std::unique_ptr<Widget5>& p2) { return *p1 < *p2; };
 
    std::vector<int> v{1, 2, 3};
    unsigned sz1 = v.size(); // 不推荐,32位和64位windows上,unsigned均是32位,而在64位windows上,std::vector<int>::size_type则是64位
    auto sz2 = v.size(); // 推荐,sz2's type is std::vector<int>::size_type
    
    std::unordered_map<std::string, int> m;
    for (const std::pair<std::string, int>& p : m) {} // 显式型别声明,不推荐,the key part of a std::unordered_map is const, so the type of std::pair in the hash table is std::<const std::string, int>, 需要进行隐式转换,会产生临时对象
    for (const auto& p : m) {} // 推荐
 
    return 0;
}

用auto声明的变量必须初始化。

在C++14中,lambda表达式的形参都可以使用auto。

std::function是C++11标准库中的一个模板。函数指针只能指涉(point)到函数,而std::function却可以指涉(refer to)任何可调用对象,即任何可以像函数一样实施调用之物。正如你若要创建一个函数指针就必须指定欲指涉到的函数的型别(即该指针指涉到的函数的签名),你若要创建一个std::function对象就必须指定欲指涉的函数的型别。

使用std::function和使用auto有所不同:使用auto声明的、存储着一个闭包(closure)的变量和该闭包是同一型别,从而它要求的内存量也和该闭包一样。而使用std::function声明的、存储着一个闭包的变量是std::function的一个实例,所以不管给定的签名(signature)如何,它都占有固定尺寸的内存,而这个尺寸对于其存储的闭包而言并不一定够用。如果是这样的话,std::function的构造函数就会分配堆上的内存来存储该闭包。从结果上看,std::function对象一般都会比使用auto声明的变量使用更多内存。通过std::function来调用闭包几乎必然会比通过使用auto声明的变量来调用同一闭包要来得慢。

std::function更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/52562918

auto也并不完美,每个auto变量的型别都是从它的初始化表达式推导出来的,而有些初始化表达式的型别既不符合期望也不符合要求。

显式的写出型别经常是画蛇添足,带来各种微妙的偏差,有些关乎正确性,有些关乎效率,或是两者都受影响。还有,auto型别可以随着其初始化表达式的型别变化而自动随之改变。

auto更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/51834927

class Widget6 {};
std::vector<bool> features(const Widget6& w)
{
    return std::vector<bool>{true, true, false, false, true, false};
}
 
void processWidget6(const Widget6& w, bool highPriority) {}
 
double calcEpsilon() { return 1.0; }
 
int test_item_6()
{
    Widget6 w;
    bool highPriority =features(w)[5]; // 正确,显式声明highPriority的型别
    processWidget6(w, highPriority);
 
    // 把highPriority从显示型别改成auto
    auto highPriority2 = features(w)[5]; // highPriority2的型别由推导而得,std::vector<bool>的operator[]的返回值并不是容器中一个元素的引用(单单bool是个例外),返回的是个std::vector<bool>::reference型别的对象,返回一个std::vector<bool>型别的临时对象
    processWidget6(w, highPriority2); // undefined behavior, highPriority2 contains dangling pointer(空悬指针)
 
    auto highPriority3 = static_cast<bool>(features(w)[5]); // 正确
    processWidget6(w, highPriority3);
 
    float ep = calcEpsilon(); // 隐式转换 double-->float,这种写法难以表明"我故意降低了函数的返回值精度"
    auto ep2 = static_cast<float>(calcEpsilon()); // 推荐
 
    return 0;
}

std::vector<bool>是vector的特殊版本,用于bool类型的元素并优化空间,存储每个值仅占用一个位而不是一个字节(each value is stored in a single bit)。

std::vector<bool>::reference是个代理类的实例。所谓代理类,就是指为了模拟或增广其它型别的类(a class that exists for the purpose of emulating and augmenting the behavior of some other type)。一个普遍的规律是,”隐形”代理类和auto无法和平共处。问题在于auto没有推导成为你想推导出来的型别。解决方案应该是强制进行另一次型别转换,这种方法称为带显式型别的初始化物习惯用法。

带显式型别的初始化物习惯用法要求使用auto声明变量,但针对初始化表达式进行强制型别转换,转换成你想要auto推导出来的型别。

class Widget7 {
public:
    Widget7(int i, bool b) {} // constructor not declaring std::initializer_list params
    Widget7(int i, double d) {}
    Widget7(std::initializer_list<long double> il) { fprintf(stdout, "std::initializer_list params\n"); }
    Widget7() = default;
    Widget7(int) {}
 
    operator float() const { return 1.0f; } // 强制转换成float型别, 注意:此函数的作用,下面的w13和w15
 
private:
    int x{0}; // fine, x's default value is 0
    int y = 0; // also fine
    //int z(0); // error
};
 
int test_item_7()
{
    int x(0); // 初始化物在小括号内
    int y = 0; // 初始化物在等号之后
    int z{0}; // 初始化物在大括号内
    int z2 = {0}; // 使用等号和大括号来指定初始化物,一般C++会把它和只有大括号的语法同样处理
 
    Widget7 w1; // call default constructor
    Widget7 w2 = w1; // not an assignment; calls copy constructor
    w1 = w2; // an assignment; calls copy operator=
 
    std::vector<int> v{1, 3, 5}; // v's initial content is 1, 3, 5
 
    std::atomic<int> ai1{0}; // fine
    std::atomic<int> ai2(0); // fine
    //std::atomic<int> ai3 = 0; // error
 
    double a{std::numeric_limits<double>::max()}, b{std::numeric_limits<double>::max()}, c{std::numeric_limits<double>::max()};
    //int sum1{a + b + c}; // error, sum of doubles may not be expressible as int
    int sum2(a + b + c); // okey(value of expression truncated to an int)
    int sum3 = a + b + c; // okey(value of expression truncated to an int)
 
    Widget7 w3(10); // call Widget7 constructor with argument 10
    Widget7 w4(); // most vexing parse! declares a function named w4 that returns a Widget7
    Widget7 w5{}; // call Widget7 constructor with no args
 
    Widget7 w6(10, true); // calls first constructor
    Widget7 w7{10, true}; // alse calls first constructor, 假设没有Widget7(std::initializer_list<long double>)构造函数
    Widget7 w8(10, 5.0); // calls second constructor
    Widget7 w9{10, 5.0}; // also calls second constructor, 假设没有Widget7(std::initializer_list<long double>)构造函数
 
    Widget7 w10{10, true}; // 使用大括号,调用的是带有std::initializer_list型别形参的构造函数(10和true被强制转换为long double)
    Widget7 w11{10, 5.0}; // 使用大括号,调用的是带有std::initializer_list型别形参的构造函数(10和5.0被强制转换为long double) 
 
    Widget7 w12(w11); // 使用小括号,调用的是拷贝构造函数
    Widget7 w13{w11}; // 使用大括号,调用的是带有std::initializer_list型别形参的构造函数(w11的返回值被强制转换成float,随后float又被强制转换成long double)
    Widget7 w14(std::move(w11)); // 使用小括号,调用的是移动构造函数
    Widget7 w15{std::move(w11)}; // 使用大括号,调用的是带有std::initializer_list型别形参的构造函数(和w13的结果理由相同)
    
    Widget7 w16{}; // call Widget7 constructor with no args,调用无参的构造函数,而非调用带有std::initializer_list型别形参的构造函数
    Widget7 w17({}); // 调用带有std::initializer_list型别形参的构造函数,传入一个空的std::initializer_list
    Widget7 w18{
    
      {}}; // 调用带有std::initializer_list型别形参的构造函数,传入一个空的std::initializer_list
 
    std::vector<int> v1(10, 20); // 调用了形参中没有任何一个具备std::initializer_list型别的构造函数,结果是:创建了一个含有10个元素的std::vector,所有的元素的值都是20
    std::vector<int> v2{10, 20}; // 调用了形参中含有std::initializer_list型别的构造函数,结果是:创建了一个含有2个元素的std::vector,元素的值分别为10和20
    fprintf(stdout, "v1 length: %d, v2 length: %d\n", v1.size(), v2.size());
 
    return 0;
}

指定初始化值的方式包括使用小括号、使用等号,或是使用大括号。

C++11引入了统一初始化(uniform initialization):单一的、至少从概念上可以用于一切场合、表达一切意思的初始化。它的基础是大括号形式或称为大括号初始化(braced initialization)。

大括号同样可以用来为非静态成员指定默认初始化值,这项能力(在C++11中新加入的能力)也可以使用”=”的初始化语法,却不能使用小括号。

不可复制的对象(如std::atomic型别的对象)可以采用大括号和小括号来进行初始化,却不能使用”=”。

大括号初始化有一项新特性,就是它禁止内建型别之间进行隐式窄化型别转换(narrowing conversion)。如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译。而采用小括号和”=”的初始化则不会进行窄化型别转换检查。

大括号初始化的另一项值得一提的特征是,它对于C++的最令人苦恼之解析语法(most vexing parse)免疫。C++规定:任何能够解析为声明的都要解析为声明,而这会带来副作用。所谓最令人苦恼之解析语法就是说,程序员本来想要以默认方式构造一个对象,结果却一不小心声明了一个函数。这个错误的根本原因在于构造函数调用语法。

:这种行为源于大括号初始化物、std::initializer_list以及构造函数重载决议之间的纠结关系。这几者之间的相互作用可以使得代码看起来是要做某一件事,但实际上是在做另一件事。如果使用大括号初始化物来初始化一个使用auto声明的变量,那么推导出来的型别就会成为std::initializer_list,尽管用其它方式使用相同的初始化物来声明变量就能够得出更符合直觉的型别。在构造函数被调用时,只要形参中没有任何一个具备std::initializer_list型别,那么小括号和大括号的意义就没有区别。即使是平常会执行复制(copy)或移动的构造函数也可能被带有std::initializer_list型别形参的构造函数劫持。只有在找不到任何办法把大括号初始化物中的实参转换成std::initializer_list模板中的型别时,编译器才会退而去检查普通的重载决议(normal overload resolution)。空大括号对表示的是”没有实参”,而非”空的std::initializer_list”。

void f8(int) { fprintf(stdout, "f8(int)\n"); }
void f8(bool) { fprintf(stdout, "f8(bool)\n"); }
void f8(void*) { fprintf(stdout, "f8(void*)\n"); }
 
class Widget8 {};
int f8_1(std::shared_ptr<Widget8> spw) { return 0; }
double f8_2(std::unique_ptr<Widget8> upw) { return 1.f; }
bool f8_3(Widget8* pw) { return false; }
 
template<typename FuncType, typename MuxType, typename PtrType>
//auto lockAddCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr)) // C++11
decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) // C++14
{
    using MuxGuard = std::lock_guard<std::mutex>; // C++11 typedef
    MuxGuard g(mutex);
    return func(ptr);
}
 
int test_item_8()
{
    f8(0); // calls f8(int), not f8(void*)
    //f8(NULL); // might not compile, but typically calls f8(int), never calls f8(void*)
    f8(nullptr); // calls f(void*) overload
 
    std::mutex f1m, f2m, f3m;
    //auto result1 = lockAndCall(f8_1, f1m, 0); // error, ‘void result1’ has incomplete type
    //auto result2 = lockAndCall(f8_2, f2m, NULL); // error: ‘void result2’ has incomplete type
    auto result3 = lockAndCall(f8_3, f3m, nullptr);
 
    return 0;
}

字面常量0的型别是0,而非指针。当C++在只能使用指针的语境中发现了一个0,它也会把它勉强解释为空指针,但说到底这是一个不得已而为之的行为。C++的基本观点还是0的型别是int,而非指针。

nullptr的优点在于,它不具备整型型别。实话实说,它也不具备指针型别(pointer type)。nullptr的实际型别是std::nullptr_t,std::nullptr_t的定义被指定为nullptr的型别。型别std::nullptr_t可以隐式转换到所有的裸指针型别(raw pointer type),这就是为何nullptr可以扮演所有型别指针的原因。

nullptr更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/51793497

class Widget9 {};
 
typedef void (*FP1)(int, const std::string&);
using FP2 = void (*)(int, const std::string&);
 
template<typename T>
using MyAllocList1 = std::list<T/*, MyAlloc<T>*/>; // C++11,  MyAllocList1<T>是std::list<T, MyAlloc<T>>的同义词
 
template<typename T>
struct MyAllocList2 { // MyAllocList<T>::type 是std::list<T, MyAlloc<T>>的同义词
    typedef std::list<T/*, MyAlloc<T>*/> type;
};
 
template<typename T>
class Widget9_2 { // Widget9_2<T>含一个MyAllocList2<T>型别的数据成员
private:
    typename MyAllocList2<T>::type list; // MyAllocList2<T>::type代表一个依赖于模板型别形参(T)的型别,所以MyAllocList2<T>::type称为带依赖型别,C++中规则之一就是带依赖型别必须前面加个typename
};
 
template<typename T>
class Widget9_1 {
private:
    MyAllocList1<T> list; // 不再有"typename"和"::type"
};
 
int test_item_9()
{
    typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS1;
    using UPtrMapSS2 = std::unique_ptr<std::unordered_map<std::string, std::string>>; // C++11, alias declaration
 
    MyAllocList1<Widget9> lw1;
    MyAllocList2<Widget9>::type lw2;
 
    typedef const char cc;
    std::remove_const<cc>::type a; // char a
    std::remove_const<const char*>::type b; // const char* b
 
    typedef int&& rval_int;
    typedef std::remove_reference<int>::type A;
 
    //std::remove_const<T>::type // C++11: const T --> T
    //std::remove_const_t<T>     // C++14中的等价物
    //template<class T>
    //using remove_const_t = typename remove_const<T>::type;
 
    //std::remove_reference<T>::type // C++11: T&/T&& --> T
    //std::remove_reference_t<T>     // C++14中的等价物
    //template<class T>
    //using remove_reference_t = typename remove_reference<T>::type;
 
    return 0;
}

别名声明可以模板化(这种情况下它们被称为别名模板,alias template),typedef就不行。

C++11以型别特征(type trait)的形式给了程序员以执行此类变换的工具。型别特征是在头文件<type_traits>给出的一整套模板。该头文件中有几十个型别特征,它们并非都是执行型别变换功能的用途,但其中派此用途的部分则提供了可预测的接口。

每个C++11中的变换std::transformation<T>::type,都有一个C++14中名为std::transformation_t的对应别名模板。

别名声明更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/81259210

std::vector<std::size_t> primeFactors(std::size_t x) { return std::vector<std::size_t>(); }
 
enum class Status; // 前置声明, 默认底层型别(underlying type)是int
enum class Status2: std::uint32_t; // Status2的底层型别是std::uint32_t
enum Color: std::uint8_t; // 不限范围的枚举型别的前置声明,底层型别是std::uint8_t
 
int test_item_10()
{
    enum Color1 { black, white, red }; // 不限范围的(unscoped)枚举型别:black, white, red所在作用域和Color1相同
    //auto white = false; // error, white already declared in this scope
    Color1 c1 = black;
 
    enum class Color2 { black2, white2, red2 }; // C++11, 限定作用域的(scoped)枚举型别:black2, white2, red2所在作用域被限定在Color2内
    auto white2 = false; // 没问题,范围内并无其它"white2"
    //Color2 c1 = black2; // 错误,范围内并无名为"black2"的枚举量
    Color2 c2 = Color2::black2; // fine
    auto c3 = Color2::black2; // also fine
 
    if (c1 < 14.5) // 将Color1型别和double型别值作比较,怪胎
        auto factors = primeFactors(c1);
 
    //if (c2 < 14.5) // 错误,不能将Color型别和double型别值作比较
    //    auto facotrs = primeFactors(c2); // 错误,不能将Color2型别传入要求std::size_t型别形参的函数
 
    return 0;
}

一个通用规则,如果在一对大括号里声明一个名字,则该名字的可见性就被限定在括号括起来的作用域内。但这个规则不适用于C++98风格的枚举型别中定义的枚举量。这些枚举量的名字属于包含着这个枚举型别的作用域,这就意味着在此作用域内不能有其它实体取相同的名字。

由于限定作用域的枚举型别是通过”enum class”声明的,所有有时它们也被称为枚举类。

限定作用域的枚举型别带来的名字空间污染降低,已经是”应该优先选择它,而不是不限范围的枚举型别”的足够理由。但是限定作用域的枚举型别还有第二个优势:它的枚举量是更强型别的(labelly typed)。不限范围的枚举型别中的枚举量可以隐式转换到整数型别(并能够从此处进一步转换到浮点型别)。。限定作用域的枚举型别可以进行前置声明,C++11中,不限范围的枚举型别也可以进行前置声明,但须得在完成一些额外工作之后。这些额外工作是由以下事实带来的:一切枚举型别在C++里都会由编译器来选择一个整数型别作为其底层型别。

为了节约使用内存,编译器通常会为枚举型别选用足够表示枚举量取值的最小底层型别。在某些情况下,编译器会用空间来换取时间,而在这样的情况下,它们可能会不选择只具备最小可容尺寸的型别,但是它们当然需要具备优化空间的能力。为了使这种设计成为可能,C++98就只提供了枚举型别定义(即列出所有枚举量)的支持,枚举型别声明则不允许。

限定作用域的枚举型别的底层型别(underlying type)是已知的,默认地是int;而对于不限范围的枚举型别,你可以指定这个底层型别。如果要指定不限范围的枚举型别的底层型别,做法和限定作用域的枚举型别一样。这样作了以后,不限范围的枚举型别也能够进行前置声明了。底层型别指定同样也可以在枚举型别定义中进行。

enum class更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/78535754

class Widget11 {
public:
    Widget11(const Widget11&) = delete;
    Widget11& operator=(const Widget11&) = delete;
 
    template<typename T>
    void processPointer(T* ptr) {}
};
 
template<>
void Widget11::processPointer<void>(void*) = delete;
 
bool isLucky(int number) { return false; } // 原始版本
bool isLucky(char) = delete; // 拒绝char型别
bool isLucky(bool) = delete; // 拒绝bool型别
bool isLucky(double) = delete; // 拒绝double和float型别
 
template<typename T>
void processPointer(T* ptr) {}
template<>
void processPointer<void>(void*) = delete; // 不可以使用void*来调用processPointer
template<>
void processPointer<char>(char*) = delete; // 不可以使用char*来调用processPointer
 
int test_item_11()
{
    //if (isLucky('a')) {} // error, call to deleted function
 
    return 0;
}

C++98中为了阻止个别成员函数的使用采取的做法是声明其为private,并且不去定义它们。在C++11中,有更好的途径来达成效果上相同的结果:使用”=delete。删除函数无法通过任何方法使用。习惯上,删除函数会被声明为public,而非private。任何函数都能成为删除函数(any function may be deleted),但只有成员函数能声明为private。还有一个妙处是删除函数能做到而private成员函数做不到的,那就是阻止那些不应该进行的模板具现(template instantiation)。

指针世界中有两个异类:一个是void*指针,因为无法对其执行提领(dereference)、自增、自减等操作。还有一个是char*指针,因为它们基本上表示的是C风格的字符串,而不是指涉到单个字符的指针。

“= delete;”更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/52475108

class Base {
public:
    virtual void doWork() {} // 基类中的虚函数
};
 
class Derived : public Base {
public:
    virtual void doWork() override {} // 改写(override)了Base:doWork(“virtual”在这可写可不写)
};
 
class Widget12 {
public:
    void doWork() & { fprintf(stdout, "&\n"); } // 这个版本的doWork仅在*this是左值时调用
    void doWork() && { fprintf(stdout, "&&\n"); } // 这个版本的doWork仅在*this是右值时调用
 
    using DataType = std::vector<double>;
    DataType& data() & { fprintf(stdout, "data() &\n"); return values; } // 对于左值Widget12型别,返回左值
    DataType data() && { fprintf(stdout, "data() &&\n"); return std::move(values); } // 对于右值Widget12型别,返回右值
 
private:
    DataType values;
};
 
Widget12 makeWidget() // 工厂函数(返回右值)
{
    Widget12 w;
    return w;
}
 
void doSomething(Widget12& w) {} // 仅接受左值的Widget12型别
void doSomething(Widget12&& w) {} // 仅接受右值的Widget12型别
 
int test_item_12()
{
    std::unique_ptr<Base> upb = std::make_unique<Derived>(); // 创建基类指针,指涉到派生类对象
    upb->doWork(); // 通过基类指针调用doWork,结果是派生类函数被调用
 
    Widget12 w; // 普通对象(左值)
    w.doWork(); // 以左值调用Widget12::doWork(即Widget12::doWork &)
    makeWidget().doWork(); // 以右值调用Widget12::doWork(即Widget12::doWork &&)
 
    auto vals1 = w.data(); // 调用Widget12::data的左值重载版本,vals1拷贝构造完成初始化
    auto vals2 = makeWidget().data(); // 调用Widget12::data的右值重载版本,vals2采用移动构造完成初始化
 
    return 0;
}

如果要改写(override)真的发生,有一系列要求必须满足:(1).基类中的函数必须是虚函数。(2).基类和派生类中的函数名字必须完全相同(析构函数例外)。(3).基类和派生类中的函数形参型别必须完全相同。(4).基类和派生类中的函数常量性(constness)必须完全相同。(5).基类和派生类中的函数返回值和异常规格(exception specification)必须兼容。除了C++98给出的这些限制,C++11又加了一条。(6).基类和派生类中的函数引用饰词(reference qualifier)必须完全相同。引用饰词是为了实现限制成员函数仅用于左值或右值。带有引用饰词的成员函数,不必是虚函数。

C++11提供了一种方法来显式地标明派生类中的函数是为了改写(override)基类版本:为其加上override声明。

成员函数引用饰词的作用就是针对发起成员函数调用的对象,即*this,加一些区分度。这和在成员函数声明末尾加一个const的情形一模一样:后者表明发起成员函数调用的对象,即*this,应为const。

override更多介绍参考:https://blog.csdn.net/fengbingchun/article/details/52304284

template<class C>
auto cbegin_(const C& container) -> decltype(std::begin(container)) // cbegin的一个实现
{
    return std::begin(container);
}
 
template<class C>
auto cend_(const C& container) -> decltype(std::end(container)) // cend的一个实现
{
    return std::end(container);
}
 
int test_item_13()
{
    std::vector<int> values{1, 10, 1000};
    auto it = std::find(values.cbegin(), values.cend(), 1983); // use cbegin and cend
    values.insert(it, 1998);
 
#ifdef _MSC_VER
    auto it2 = std::find(std::cbegin(values), std::cend(values), 1983); // C++14,非成员函数版本的cbegin, cend, gcc 4.9.4 don't support
    values.insert(it2, 1998);
#endif
 
    auto it3 = std::find(cbegin_(values), cend_(values), 1983);
    values.insert(it3, 1998);
 
    return 0;
}

const_iterator是STL中相当于指涉到const的指针的等价物。它们指涉到不可被修改的值。

int f14_1(int x) throw() { return 1; } // f14_1不会发射异常: C++98风格
int f14_2(int x) noexcept { return 2; } // f14_2不会发射异常: C++11风格
 
//RetType function(params) noexcept; // 最优化
//RetType function(params) throw(); // 优化不够
//RetType function(params); // 优化不够
 
int test_item_14()
{
    return 0;
}

在C++11中,无条件的noexcept就是为了不会发射异常(emit exception)的函数而准备的。函数是否带有noexcept声明,就和成员函数是否带有const声明是同等重要的信息。当你明明知道一个函数不会发射异常却未给它加上noexcept声明的话,这就是接口规格缺陷。对不会发射异常的函数应用noexcept声明还有一个动机,那就是它可以让编译器生成更好的目标代码。

在带有noexcept声明的函数中,优化器不需要在异常传出函数的前提下,将执行期栈(runtime stack)保持在可开解状态(unwindable state);也不需要在异常溢出函数的前提下,保证所有其中的对象以其被构造顺序的逆序完成析构。而那些以”throw()”异常规格(exception specification)声明的函数就享受不到这样的优化灵活性,和没有加异常规格声明的函数一样。

// pow前面写的那个constexpr并不表明pow要返回一个const值,它表明的是如果base和exp是编译期常量,pow的返回结果
// 就可以当一个编译期常量使用;如果base和exp中有一个不是编译期常量,则pow的返回结果就将在执行期计算
constexpr int pow(int base, int exp) noexcept // pow is a constexpr func that never throws
{
    return (exp == 0 ? 1 : base * pow(base, exp - 1)); // C++11
    //auto result = 1; // C++14
    //for (int i = 0; i < exp; ++i) result *= base;
    //return result;
}
 
auto readFromDB(const std::string& str) { return 1; }
 
class Point15 {
public:
    constexpr Point15(double xVal = 0, double yVal = 0) noexcept : x(xVal), y(yVal) {}
    constexpr double xValue() const noexcept { return x; }
    constexpr double yValue() const noexcept { return y; }
    void setX(double newX) noexcept { x = newX; }
    //constexpr void setX(double newX) noexcept { x = newX; } // C++14
    void setY(double newY) noexcept { y = newY; }
    //constexpr void setY(double newY) noexcept { y = newY; } // C++14
private:
    double x, y;
};
 
constexpr Point15 midpoint(const Point15& p1, const Point15& p2) noexcept
{
    return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2}; // call constexpr member function
}
 
int test_item_15()
{
    int sz = 0; // non-constexpr variable
    //constexpr auto arraySize1 = sz; // error, sz's value not known at compilation
    //std::array<int, sz> data1; // error, sz's value not known at compilation
    constexpr auto arraySize2 = 10; // fine, 10 is a compile-time constant
    std::array<int, arraySize2> data2; // fine, arraySize2 is constexpr
 
    // 注意:const对象不一定经由编译器已知值来初始化
    const auto arraySize3 = sz; // fine, arraySize3 is const copy of sz,arraySize3是sz的一个const副本
    //std::array<int arraySize3> data3; // error, arraySize3.s value not known at compilation
 
    constexpr auto numConds = 5;
    std::array<int, pow(3, numConds)> results; // results has 3^numConds elements
 
    auto base = readFromDB("base"); // get these values at runtime
    auto exp = readFromDB("exponent");
    auto baseToExp = pow(base, exp); // call pow function at runtime
 
    constexpr Point15 p1(9.4, 27.7); // fine, "runs" constexpr constructor during compilation
    constexpr Point15 p2(28.8, 5.3);
 
    constexpr auto mid = midpoint(p1, p2); // 使用constexpr函数的结果来初始化constexpr对象
 
    return 0;
}

当constexpr应用于对象时,其实就是一个加强版的const。但应用于函数时,你既不能断定它是const,也不能假定其值在编译阶段就已知。

所有constexpr对象都是const对象,但并非所有的const对象都是constexpr对象。如果你想让编译器提供保证,让变量拥有一个值,用于要求编译期常量的语境,那么能达到这个目的的工具是constexpr,而非const。

constexpr函数可以用在要求编译期常量的语境中。在这样的语境中,若你传给一个constexpr函数的实参值是在编译期已知的,则结果也会在编译期计算出来。如果任何一个实参值在编译期未知,则你的代码将无法通过编译。在调用constexpr函数时,若传入的值有一个或多个在编译期未知,则它的运作方式和普通函数无异,亦即它也是在运行期执行结果的计算。

class Point16 { // 使用std::atomic型别的对象来计算调用次数
public:
    double distanceFromOrigin() const noexcept
    {
        ++callCount; // 带原子性的自增操作
        return std::sqrt((x*x) + (y*y));
    }
 
private:
    mutable std::atomic<unsigned> callCount{0};
    double x, y;
};
 
class Widget16 {
public:
    int magicValue() const
    {
        std::lock_guard<std::mutex> guard(m); // lock m
        if (cacheValid) return cachedValue;
        else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cachedValue = val1 + val2;
            cacheValid = true;
            return cachedValue;
        }
    } // unlock m
 
private:
    int expensiveComputation1() const { return 1; }
    int expensiveComputation2() const { return 2; }
 
    mutable std::mutex m;
    mutable int cachedValue; // no longer atomic
    mutable bool cacheValid{false};
};
 
int test_item_16()
{
    return 0;
}

对于单个要求同步的变量或内存区域,使用std::atomic就足够了。但是如果有两个或更多个变量或内存区域需要作为一整个单位进行操作时,就要动用互斥量了。

std::mutex是个只移型别(std::mutex is a move-only type)(i.e., a type that can be moved, but not copied)。与std::mutex一样,std::atomic也是只移型别。

class Widget17 {
public:
    Widget17(Widget17&& rhs); // move constructor
    Widget17& operator=(Widget17&& rhs); // move assignment operator
 
    Widget17(const Widget17&) = default; // default copy constructor, behavior is OK
    Widget17& operator=(const Widget17&) = default; // default copy assign, behavior is OK
};
 
int test_item_17()
{
    return 0;
}

在C++官方用语中,特种成员函数是指那些C++会自行生成的成员函数。C++98有四种特种成员函数:亦即,在某些代码使用了它们,而在类中并未显式声明的场合。仅当一个类没有声明任何构造函数时,才会生成默认构造函数(只要指定了一个要求传参的构造函数,就会阻止编译器生成默认构造函数)。生成的特种成员函数都具有public访问层级且是inline的,而且它们都是非虚的,除非讨论的是一个析构函数,位于一个派生类中,并且基类的析构函数是个虚函数。在那种情况下,编译器为派生类生成的析构函数也是个虚函数。

在C++11中,特种成员函数加入了两位新成员:移动构造函数和移动赋值运算符。移动操作也仅在需要时才生成,而一旦生成,它们执行的也是作用于非静态成员的”按成员移动”操作。意思是,移动构造函数将依照其形参rhs的各个非静态成员对于本类的对应成员执行移动构造,而移动赋值运算符则将依照其形参rhs的各个非静态成员对于本类的对应成员执行移动赋值。移动构造函数同时还会移动构造它的基类部分(如果有的话),而移动赋值运算符则会移动赋值它的基类部分。不过,当我提到移动操作在某个数据成员或基类部分上执行移动构造或移动赋值的时候,并不能保证移动操作真的会发生。”按成员移动(Memberwise moves)”实际上更像是按成员的移动请求,因为那些不可移动的型别(即那些并未为移动操作提供特殊支持的型别,这包括了大多数C++98中的遗留型别)将通过其拷贝操作实现”移动”。

两种拷贝操作是彼此独立的(the two copy operations are independent):声明了其中一个,并不会阻止编译器生成另一个。两种移动操作并不彼此独立(the two move operations are not independent):声明了其中一个,就会阻止编译器生成另一个。此外,一旦显式声明了拷贝操作,这个类也就不再会生成移动操作了。反之亦然,一旦声明了移动操作(无论是移动构造还是移动赋

标签: spw321传感器w4g传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台