关于c++:学懂现代CEffective-Modern-C之转向现代C

4次阅读

共计 5760 个字符,预计需要花费 15 分钟才能阅读完成。

前言

古代 C ++ 中像 auto、智能指针、挪动语义等都是一些重大的优化个性,但也有一些像 constexpr、nullptr 等等这样一个小的个性。这章的内容就是这些小个性的汇合。

条款 7:在创建对象时留神辨别 () 和{}

在古代 C ++ 中有 3 种形式来以指定的值初始化对象,别离时小括号、等号和大括号:

int x(0);  // 初始化值在小括号中
int y = 0; // 初始化值在等号后
int z{0};  // 初始化值在大括号中

其中,大括号模式的初始化时 C ++11 引入的对立初始化形式。大括号初始化能够利用的语境最为宽泛,能够阻止隐式窄化的类型转换,还对最令人苦恼的解析语法免疫。

先说阻止隐式窄化的类型转换,比方上面代码能够通过编译:

double x,y,z;

int sum1(x+y+z);  // 能够通过编译,表达式的值被截断为 int
int sum2 = x+y+z; // 同上

而以下代码不能够通过编译,因为大括号初始化禁止内建类型间接进行隐式窄化类型的转换。

int sum3{x+y+z};  // 编译不通过

再说最令人苦恼的解析语法免疫。C++ 规定:任何可能解析为申明的都要解析为申明,而这会带来副作用。所谓最令人库娜的解析语法就是——程序员原本想要以默认形式结构一个对象,后果却不小心申明了一个函数。举个例子,我想调用一个没有形参的 Widget 构造函数,如果写成 Widget w();, 那后果就变成了申明了一个函数(名为 w,返回一个 Widget 类型对象)而非对象。而用大括号初始化Widget w{}; 就不存在这个问题了。

然而,不能自觉的都应用大括号初始化。在构造函数被调用时,只有形参中没有任何一个具备 std::initializer_list 类型,那么大括号和小括号没有区别;如果又一个或多个构造函数申明了任何一个具备 std::initializer_list 类型的形参,那么采纳了大括号初始化语法的调用语句会强烈地优先选用带有 std::initializer_list 类型形参的重载版本。也就是说,因为 std::initializer_list 的存在,大括号初始化和小括号初始化会产生天壤之别的后果。

这点最突出的例子是:应用两个实参来创立一个 std::vector< 数值类型 > 对象。std::vector 有一个两个参数的构造函数,容许指定容器的初始大小(第一个参数),以及所有元素的初始值(第二个参数);但它还有一个 std::initializer_list 类型形参的构造函数。如果要创立一个元素为数值类型的 std::vector(比方 std::vector<int>), 并且传递两个实参给构造函数,那么应用大括号和小括号初始化的差异就比拟大了:

std::vector<int> v1(10, 20); // 创立一个含有 10 个元素的 vector,所有元素的初始值都是 20

std::vector<int> v1{10, 20}; // 创立一个含有 2 个元素的 vector,元素的值别离时 1,20

所以,如果是作为一个类的作者,最好把构造函数设计成客户无论应用小括号还是大括号都不会影响调用得重载版本才好。

条款 8:优先选用 nullptr,而非 0 或 NULL

因为 0 和 NULL 都不是指针类型,而 nullptr 才是真正的指针类型。比方在重载指针类型和整型的函数时,如果应用 0 或者 NULL 调用这样的重载函数,则永远不会调用到指针类型的重载版本,只有应用 nullptr 能力调用到。当然为了兼容咱们依然须要遵循 C ++98 的领导准则:防止在整型和指针类型之间重载。

条款 9:优先选用别名申明,而非 typedef

C++11 提供了别名申明来替换 typedef,两者作用在大部分状况下是一样的。比方上面的 typedef:

typedef std::unique_ptr<<std::unordered_map<std::string, std::string>>> UPtrMapSS;

typedef void (*FP)(int, const std::string&);

能够用上面的别名申明来替换:

using UPtrMapSS = std::unique_ptr<<std::unordered_map<std::string, std::string>>>;

using FP = void (*)(int, const std::string&);

但还有有一种场景是只能应用别名申明的,那就是在定义模板的时候,typedef 不反对模板化,但别名申明反对。在 C ++98 中须要用嵌套在模板化的 struct 里的 typedef 能力达到雷同成果。比方上面这段:

template<typename T>
struct MyAllocList {typedef std::list<T, MyAlloc<T>> type; //MyAllocList<T>::type 是 std::list<T, MyAlloc<T>> 的同义词};

MyAllocList<Widget>::type lw; // 客户代码

在 C ++11 中用别名申明就很简略了:

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; //MyAllocList<T> 是 std::list<T, MyAlloc<T>> 的同义词

MyAllocList<Widget> lw; // 客户代码

这里还能够看到,别名模板能够让人免写“::type”后缀。并且在模板内,对于内嵌 typedef 的援用常常要求加上 typename 的前缀,而别名模板没有这个要求。

条款 10:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型

举荐优先选用 C ++11 提供的限定作用域的枚举类型有 3 个理由。第一, 它能够升高名字空间的净化,因为限定作用域的枚举类型仅在枚举类型内可见。比方上面 C ++98 的代码会报错:

enum Color {black, white, red}; // black、white、red 和 Color 所在作用域雷同

auto white = false; // 编译报错!white 在后面曾经申明

而相似代码选用限定作用域的枚举类型则不会有问题:

enum class Color {black, white, red}; // black、white、red 所在作用域限定在 Color 内

auto white = false; // 没有问题

第二,它的枚举量是更强类型的,只能通过强制类型转换以转换为其余类型。这样能够防止奇怪的应用枚举值与数值类型比拟的代码,真要应用时也必须进行一次强制转换来揭示这里有一个顺当的比拟。

第三,限定作用域的枚举类型总是能够进行前置申明,而不限作用域的枚举类型却只有在指定了默认底层类型的前提下才能够进行前置申明。

还有一点须要记住,这两种枚举类型都反对指定底层类型。限定作用域的枚举类型默认底层类型是 int。而不限作用域的枚举类型则没有默认底层类型,编译器会为枚举类型抉择足够示意枚举值的最小类型,这也是为什么它不能间接进行前置申明,在没定义前编译器无奈确认底层类型的。

条款 11:优先选用删除函数,而非 private 未定义函数

C++11 提供了应用“=delete”的办法将想阻止客户调用得函数标识为删除函数的办法,用以代替 C ++98 中传统的将这些函数申明为 private 的办法。

删除函数的一个重要长处在于,任何函数都能成为删除函数,包含非成员函数和模板的具现。比方, 我想定义一个判断是否是侥幸数字的函数,因为隐式转换的存在会有一些奇怪的调用,而将他们定义为删除函数后就能够阻止这些奇怪的调用:

bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;

// 上面奇怪的调用无奈通过编译
if (isLucky('a')) ...
if (isLucky(true)) ...
if (isLucky(3.5)) ...

事实上,C++98 中把函数申明为 private 并且不去定义,这样的实际想要的就是 C ++11 中的删除函数理论达到的成果。前者作为后者的一种模仿动作,当然不如本尊来的好用。比方,前者无奈利用于类内部的函数,也不总是可能利用于类外部的函数(类外部的函数模板)。就是它能用,也可能直到链接阶段才发挥作用。所以,请始终应用删除函数。

条款 12:为意在改写的函数增加 override 申明

因为对于申明派生类中的改写,保障正确性很重要,而出错又很容易,C++11 提供了一种办法来显示地标识派生类中的函数时为了改写基类版本:为其加上 override。这样如果派生类中的改写出错,编译器在编译阶段就会报错。

它还有一个益处就是能够在你打算更改基类中虚函数的签名时,掂量以下波及的影响面。

条款 13:优先选用 const_iterator,而非 iterator

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

C++11 中获取和应用 const_iterator 相比于 C ++98 变得很容易了。容器的成员函数 cbegin 和 cend 都返回 const_iterator 类型,甚至对于非 const 容器也是如此,并且 STL 成员函数若要取用批示地位的迭代器(例如,作插入或删除只用),它们也要求应用 const_iterator 类型。上面是一段 C ++11 中应用 const_iterator 的示例代码:

std::vector<int> values;

auto it = std::find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1988);

条款 14:只有函数不会抛出异样,就为其加上 noexcept 申明

当你明晓得一个函数不会抛出异样却未给它加上 noexcept 申明的话,这就是接口规格缺点。对于不会抛出异样的函数利用 noexcept 申明还有一个动机,那就是它能够让编译器生成更好的指标代码。绝对于不带 noexcept 申明的函数,它有更多机会的失去优化。

noexcept 性质对于挪动操作,swap、内存开释函数和析构函数最有价值。默认地,内存开释函数和所有的析构函数都隐式地具备 noexcept 性质。

大多数函数都是异样中立的。此类函数本身并不抛出异样,但它们调用得函数则可能会抛出异样。当这种状况真的产生时,异样中立函数会容许该异样经由它传至调用栈的更深一层。异样中立函数用于不具备 noexcept 性质,因为它们可能会抛出这种“路过”的异样。

条款 15:只有有可能应用 constexpr,就应用它

constexpr 对象都具备 const 属性,并由编译期已知的值实现初始化。所有的 constexpr 对象都是 const 对象,而并非所有的 const 对象都是 constexpr 对象。

constexpr 函数在调用时若传入的实参值时编译器已知的,则会产生编译器后果。如果传入的值有一个或多个在编译期未知,则它的运作形式和一般函数无异,亦即它也是在运行期执行后果的计算。

在 C ++11 中,constexpr 函数不得蕴含多余一个可执行语句,即一条 return 语句。但在 C ++14 中,这种限度被大大地放宽了,能够有多条语句。

条款 16:保障 const 成员函数的线程安全性

保障 const 成员函数的线程安全性,除非能够确信它们不会用在并发语境中。

使用 std::atomic 类型的变量会比使用互斥量有更好的性能,因为其开销往往较小。

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

条款 17:了解特种成员函数的生成机制

特种成员函数是指那些 C ++ 会自行生成的成员函数:默认构造函数、析构函数、复制操作和挪动操作。其中挪动操作时 C ++11 新增的,包含两个成员——挪动构造函数和挪动赋值运算符。示例如下:

class Widget {
public:
...
Widget(Widget&& rhs);               // 挪动构造函数
Widget& operator=(Widget&& rhs);    // 挪动赋值运算符
}

C++11 中,特种成员函数的生成机制如下:

  • 默认构造函数:与 C ++98 的机制雷同。仅当类中不蕴含用户申明的构造函数时才生成。
  • 析构函数:与 C ++98 的机制基本相同,惟一的区别在于析构函数默认为 noexcept。与 C ++98 的机制雷同的是,仅当基类的析构函数为虚的,派生类的析构函数才是虚的。
  • 复制构造函数和复制赋值运算符:运行期行为与 C ++98 雷同——按成员进行非静态数据成员的复制结构和复制赋值。复制构造函数仅当类中不蕴含用户申明的复制构造函数时才生成,如果该类申明了挪动操作则复制构造函数将被删除。复制赋值运算符仅当类中不蕴含用户申明的复制赋值运算符时才生成,如果该类申明了挪动操作则复制赋值运算符将被删除。在曾经存在显示申明的析构函数的条件下,生成复制操作曾经成为了被废除的行为。
  • 挪动构造函数和挪动赋值运算符:都按成员进行非静态数据成员的挪动操作。仅当类中不蕴含用户申明的复制操作、挪动操作和析构函数时才生成。

综上,如果想申明一个基类,且提供默认的挪动操作和复制操作,则须要如下定义:

class Base {
public:
    virtual ~Base() = default;
    Base(Base&&) = default; // 提供挪动操作
    Base& operator=(Base &&) = default;
    
    Base(const Base&) = default; // 提供复制操作
    Base& operator=(const Base &) = default;
}

这里解释一下:通常状况下,虚析构函数的默认实现就是正确的,而“=default”则是表白这一点的很好形式。不过,一旦用户申明了析构函数,挪动操作的生成就被克制了,而如果可移动性是可能反对的,加上“=default”就可能再次给予编译器以生成挪动操作的机会。申明挪动操作又会破除复制操作,所以如果还要可复制性,就再加一轮“=default”。

还有一点须要留神的是,成员函数模板在任何状况下都不会已知特种成员函数的生成。

正文完
 0