《Effective Modern C++》学习总结(条款11- 15)

条款11:优先使用delete关键字删除函数而不是private却又不实现的函数

1.=delete 是C++ 11新特性——见侯捷C++ 九中的描述

  • 删除的函数不能通过任何方式被使用

  • 方便起见,删除函数被声明为公有的,而不是私有的。这样设计的原因是,使这个函数为公有的可以产生更易读的错误信息

  • 任何函数都可以是删除的,然而仅有成员函数才可以是私有的

2.小技巧1:使用delete能够防止隐式转换(见第一部分侯捷C++ 九)——通过把隐式转换的重载函数都delete,避免隐式转换,充分利用了delete和重载函数决议的规则

3.小技巧2:使用delete可以避免不必要的模板具现化

template<typename T>
void processPointer(T* ptr)
  • 对于上面这个模板,我们不需要对void*char*做处理(这两个指针都较为特殊,前者是因为没有办法对它们解引用,递
    增或者递减它等操作。后者往往表示指向c类型的字符串,而void*不是指向独立字符的指针),因此我们必须禁止void*char*的具现化,这个时候只要结合模板特化和delete即可实现这一功能。
template<>
void processPointer<void>(void*) = delete;

template<>
void processPointer<char>(char*) = delete;
  • 现在当我们使用这个模板的时候就无法具现化成void*char*了,而这个功能是C++98无法做到的, 对于C++98来说实现的唯一途径就是借助于成员函数的访问权限,然后模板特化是无法在类的作用域内定义的。

4.请记住:

  • 优先使用delete来删除函数替换放在私有作用域中未定义的;
  • 任何函数都可以被删除,包括非成员函数,模版实例化等。

 

条款12:使用override关键字声明覆盖的函数

1.override使用的目的:防止虚函数在使用的时候因为各种原因无法正常完成覆写

2.如果要使用覆盖的函数,几个条件必须满足:

  • 基类中的函数被声明为虚的。

  • 基类中和派生出的函数必须是完全一样的(除了虚析构函数)。

  • 基类中和派生出的函数的参数类型必须完全一样。

  • 基类中和派生出的函数的常量特性必须完全一样。

  • 基类中和派生出的函数的返回值类型和异常声明必须是兼容的。

  • 函数的引用修饰符必须完全一样。成员函数的引用修饰符是很少被提及的的C++特性,这些修饰符使得将这些函数只能被左值或者右值使用成为可能。成员函数不需要声明为虚就可以使用它们。引用标识符有两个,一个是 &,用这个修饰的成员函数只允许被*this是左值来调用。另外一个是&&,值允许被*this是右值来调用。

class	Widget{ 
public:
	...
	void doWork() &;								//只有当*this为左值时 
													//这个版本的doWorkd()函数被调用
	void doWork() &&;								//只有当*this为右值 
													//这个版本的doWork()函数被调用 
};
Widget makeWidget();								//工厂函数,返回右值 
Widget w;											//正常的对象(左值)
...
w.doWork();											//为左值调用Widget::doWork()	即Widget::doWork	&
makeWidget().doWork();								//为右值调用Widget::doWork()	即Widget::doWork	&&

3.一些正确的示范——virtual函数的覆写

class Base { 
public:
	virtual void mf1() const;
	virtual	void mf2(int x);
	virtual	void mf3() &;
	virtual	void mf4() const; 
};
class Derived: public Base { 
public:
	virtual	void mf1() const override;
	virtual	void mf2(int x)	override;
	virtual	void mf3() & override;
	void mf4() const override;						//	加上"virtual"也可以,但是不是必须的
};

4.请记住:

  • 对于要重写的函数添加override关键字,让编译器负责检查;
  • 成员函数的引用标识符可以识别出(*this)的不同,是左值类型,还是右值类型。

 

条款13:优先使用const_iterator而不是iterator

1.标准实践中,当你需要迭代器并且不需要更改迭代器指向的值的时候,你应该使用const_iterator

2.C++ 11 中的const_iterator既容易获得也容易使用

  • 容器中成员函数cbegin()cend可以产生const_iterator,甚至非const容器也可以这样做
  • STL成员函数通常使用const_iterator来进行定位(即插入与删除)

3.一个通用模板代码:

template <typename C, typename V>
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {
	using std::cbegin;
    using std::end;

    auto it = std::find(cbegin(container), cend(container), targetVal);
    container.insert(it, insertVal);
}
  • 上面的代码就比较通用了,对于一切类容器的数据结构都是适用的,而不是只能用于标准的STL容器了。很可惜上面的代码需要C++14的支持,C++11只支持beginend,而到C++14才开始支持cbegincend。如果你的编译器不支持C++14,那么你可以使用下面这段代码代替。

4.请记住:

  • 优先使用const_iterator替换iterator
  • 为了是代码更通用,应该优先使用非成员函数版本的beginendcbegincend

 

第四章:智能指针

 

条款14:将不会抛出异常的函数声明为 noexcept

在任何时候,只要知道函数不会产生异常,就声明为noexcept

1.在 C++ 11 中,绝对的 noexcept 用于修饰保证不会发出异常的函数

  • 函数是否为 noexcept 与成员函数是否为常量同样重要。当你知道一个函数不会触发异常,却没有声明为 noexcept,那就是糟糕的接口设计
  • 将函数声明为 noexcept 的另一个原因是:它允许编译器生成更好的目标代码
int f(int x) throw()    // C++98 style —— less optimizable
int f(int x) noexcept   // C++ 11 style —— most optimizable
  • 使用throw()来表示不抛出异常的情况下,如果函数抛出异常会导致栈解旋,然后程序结束这也就是所谓的 less optimizable
  • 而使用noexcept()的情况下,程序会直接结束。这两种形式对最后生成的代码有很大的影响。对于后者就不需要生成一些用于保护堆栈的代码了,因为不需要解旋,所以对代码优化友好—— most optimizable

2.举例:用move代替拷贝std::vector元素

  • 见第二部分二十三、二十五、二十六;

  • 为了优化vector内部的元素拷贝,C++11是将原来空间中的元素一个个move到新的空间中,如果在move的过程中发生了异常,那么这将是一个不可逆的过程,并且失去了C++98中异常安全保证的能力,因此C++11需要知道move操作是否是一个不会抛出异常的操作,如果是才会使用move;

  • 这会带来性能上的提升,否则就降级到C++98的场景下,通过拷贝来实现,保证异常安全。如果move 是用了noexcept关键字声明的,那么就可以使用标准库提供的noexcept()函数来检测move是否会抛出异常,而这个在C++98中是做不到的。

3.请记住:

  • noexcept是函数接口的一部分,这意味着调用者会依赖它;
  • 使用noexcept声明的函数相比于没有使用noexcept声明的函数代码更具可优化性;
  • noexpect对于moveswap、内存分配函数、析构函数等具有特别的价值;
  • 大多数函数都是异常中立的而不是noexcept

 

条款15:尽可能地使用constexpr

1.当constexpr用于对象时,它本质上是const的一种增强形式

2.当constexpr用于函数时:

  • constexpr修饰的函数可以使得该函数可以在编译期运行,产生一个编译期的值,也可以像普通的函数一样在运行期执行,然后产生一个运行期的值。这很大程度上取决于传入的参数是编译期的值还是运行期的值。
  • constexpr 函数仅限于接受和返回字面类型(literal type),实际上就是可以在编译期间确定值的类型,内置类型中除了void都是LiteralType类型,自定义类型也可以是LiteralType类型,这取决于其自定义类型的构造函数是否是constexpr
class Point {
public:
    constexpr Point(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; }
    void setY(double newY) noexcept { y = newY; }
private:
    double x,y;
};

3.从概念上讲,constexpr修饰的值不仅仅是一个常量值,而且还是一个编译期就知道的值

4.与const的区别

  • const修饰的变量可以是编译期的值,也可以是运行期的值。而constexpr必须是编译期的值

  • const只能用于修饰类的成员函数,而constexpr可以修饰普通的函数,并且两者的含义完全不同。

  • 所有constexpr对象都是const,但不是所有const对象都是constexpr

int sz;
constexpr auto arraySize1 = sz;     // error sz's value not known at compilation
std::array<int, sz> data1           // error array size need compilation value
constexpr auto arraySize2 = 10;     // fine
std::array<int, arraySize2> data2;  // fine
  • 希望编译器保证变量的值可以在需要编译时常量的上下文中使用,那么需要使用的工具是 constexpr,而不是 const

  • C++中有些地方必须要传入编译期的值,比如数组的大小,˙整型模版参数(标准库的bitset容器),枚举值,对齐大小等等。在这些地方如果你想传入一个编译期的值,const是无法保证的,除非你确定你const修饰的变量都是编译期的值,倘若后面修改代码使得变成非编译期的值,将会导致大量编译错误,而这些地方使用constexpr是最万无一失的了,因为编译器会确保constexpr是编译期值。

5.请记住:

  • constexpr对象是const,它的初始值是编译期的;
  • constexpr函数当传入的参数是编译期值时可以产生编译期的结果;
  • constexpr对象和函数可以广泛使用在非constexpr修饰的对象和函数上下文中;
  • constexpr关键字是对象以及函数接口的一部分。
上一篇:C++语言导学 第一章 基础知识 - 1.6 常量


下一篇:C++ const (二) constexpr和常量表达式,constexpr函数