条款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只支持begin
和end
,而到C++14才开始支持cbegin
,cend
。如果你的编译器不支持C++14,那么你可以使用下面这段代码代替。
4.请记住:
- 优先使用
const_iterator
替换iterator
- 为了是代码更通用,应该优先使用非成员函数版本的
begin
、end
、cbegin
、cend
等
第四章:智能指针
条款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
对于move
、swap
、内存分配函数、析构函数等具有特别的价值; - 大多数函数都是异常中立的而不是
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
关键字是对象以及函数接口的一部分。