Chapter 3-2: Moving to Modern C++
Item 11: Prefer deleted functions to private undefined ones
C++中,有些函数我们不想让使用者直接调用,一般来说可以直接声明成private
的类型。在这里,我们重点考虑允许调用复制构造函数和复制赋值符号。
在C++98中,如果不想让上述情况发生,只需要把复制构造函数和复制赋值符号声明成private
但不实现即可。
class Ex {
public:
//........
private:
Ex(const Ex&);
Ex& operator=(const Ex&); // 注意这里返回引用了,虽然函数体无定义。
};
但是,有的编译器(比如MSVC2017)会对上述的声明报错为函数体未定义,或者在其他函数(成员函数或者友元)中调用也会报错。
在C++11中,更好的解决方法为声明成deleted function,使用=delete
:
class Ex {
public:
Ex(const Ex&) = delete;
Ex& operator=(const Ex&) = delete;
};
deleted function与C++98的方式除了形式上的不同之外,还有一个最大的区别是deleted function不能被任何函数调用,包括成员函数和友元函数。deleted function最好声明为public
。因为编译的时候会先检查函数的属性是public
、private
或者protected
, 当用户从外界直接调用这些函数的时候,有些编译器只是会报错这些是不可访问的,而声明为public
会直接显示更详细的错误信息。
deleted function的一个应用为过滤类型重载:
bool isLucky(int num); // 原始函数
bool isLucky(char) = delete; // 拒绝char
bool isLucky(double) = delete; // 拒绝double和float
bool isLucky(bool) = delete; // 拒绝bool
float
更倾向于转化成double
而不是int
。在参数类型转换之前,编译器会先检查重载,如果没有该类型的重载,再进行类型转换。
deleted function的另一个应用是阻止不应该被实例化的模板类型的实例化:
template<typename T> // 实际上,在这里更推荐使用智能指针,不过这只是为了举例说明。
void processPointer(T* ptr);
C++中有两种特殊的指针例子:void*
和char*
。对于前者,我们无法对它进行任何的引用、递增或者递减等的操作;后者,是一个C
风格的字符串,而不是指向单个字符的指针!因此该模板应该拒绝使用这两种类型的指针。最好的方法是把这两种情况的函数声明为=deleted
:
#include <iostream>
class Ex {
public:
template<typename T>
void processPointer(T* ptr);
};
template<> // 单独声明为deleted function,可以不使用typename T,因为已经实例化
void Ex::processPointer(void* ptr) = delete;
template<> // 同上
void Ex::processPointer(char* ptr) = delete;
template<typename T> // 一般情况
void Ex::processPointer(T* ptr) {
std::cout << *ptr << std::endl;
}
int main() {
char* ch = "test";
int a{0};
Ex ex;
ex.processPointer(ch); // 编译报错:使用了deleted function
ex.processPointer(&a); // 正常运行
return 0;
}
注意一点:template
的实例化必须写在命名空间中,不能在class
里面。因此如果C++98处理上述情况会比较麻烦。
总结:
- 使用deleted function而不是私有的未定义的函数
- 任何函数都可以是deleted function,包括没有具体成员的函数和模板的实例化函数
##Item 12: Declare overriding functions override.
一个重写的例子:
#include <memory>
#include <iostream>
class Base {
public:
virtual void doWork() { // 基类的虚函数
std::cout << "Base" << std::endl;
}
};
class Derived: public Base {
public:
virtual void doWork() { // 重写基类的构造函数,virtual可以省略,但建议不省略
std::cout << "Derived" << std::endl;
}
};
int main() {
std::unique_ptr<Base> upb = // 对派生类创建基类指针,在后续章节提到
std::make_unique<Derived>();
upb->doWork(); // 调用基类的指针,派生类的指针被调用
return 0;
}
//输出 Derived
重写和重载的异同:重写和重载都是使相同的函数具有不同的功能; 重写是垂直的关系,即两个函数处于父类和子类的关系,重载是水平的关系,即两个函数在同一个类和方法中。
C++11中满足重写的条件:
- 基类函数必须是需函数
- 基类和派生类函数的名称、参数、
const
必须一致 - 返回类型和
exception
必须一致 - 函数的reference qualifiers必须一致(暂时放在后面论述)
但是,在重写函数的时候,我们可能会忽略一些重写满足的条件,而此时编译器不会报错,只是有警告,比如:
class Base {
public:
virtual void doWork()const { // 基类的虚函数
std::cout << "Base" << std::endl;
}
};
class Derived: public Base {
public:
virtual void doWork() { // 无法重写了,不满足条件,但是编译通过
std::cout << "Derived" << std::endl;
}
};
解决办法是在重写后的函数名后面添加关键字override
,用于显式检查。
class Base {
public:
virtual void doWork()const { // 基类的虚函数
std::cout << "Base" << std::endl;
}
};
class Derived: public Base {
public:
virtual void doWork()override { // 无法重写了,不满足条件,编译不通过。virtual可以省略
std::cout << "Derived" << std::endl;
}
};
override
关键字只有在函数名称最后使用才有效,如果之前的代码有函数名为override
的函数,不会影响函数的功能。
class Widget {};
void doSomething(Widget& w); // 只接受左值
void doSomething(Widget&& w); // 只接受右值
假设有这样一个类:
class Widget {
public:
using DataType = std::vector<double>;
DataType& data() {
return values;
}
private:
DataType values;
};
客户代码:
Widget w;
auto vals1 = w.data(); // 把w.values的值复制给vals1
在这里,因为返回值是引用,所以w.data
返回一个左值。但是由于auto
的性质是忽略掉引用的部分,因此还是需要把左值复制给vals1
。
假设有一个工厂函数,专门用于创建Widget
:
Widget makeWIdget(); // 创建并返回一个Widget,是右值
auto vals2 = makeWidget().data(); // vals2从产生的临时widget对象中复制values
因此,在工厂函数返回的过程中,从临时Widget
中需要复制一遍values
,但是这是一种浪费时间的操作,不如直接把该临时变量Widget
的value
当作右值进行引用处理。使用reference aualifiers可以完成这种转换:
class Widget {
public:
using DataType = std::vector<double>;
DataType& data()& { // 对于左值,返回左值
return values;
}
DataType data()&& { // 对于右值,返回右值
return std::move(values);
}
void p() {
std::cout << values[0] << std::endl;
}
private:
DataType values;
};
客户代码:
auto vals1 = w.data(); // 调用左值的重载,但还是复制vals1
auto vals2 = makeWidget().data();// 调用右值的重载,
总结:
- 重载用
override
说明 - 成员函数reference qualifiers可以区别对待左值和右值对象。
##Item 13 Prefer const_iterators to iterators
按理说,const_iterator
的使用就像const
在STL中的使用一样, 它指向的元素的值不能被改变。使用const_iterator
的标准就像使用const
的标准一样,只要我们没有想更改的值,就用常类型来迭代。但是,在C++中使用const_iterator
会有一些隐含的问题,这在C++98中更加明显。假设有下面的代码:
std::vector<int> values;
//....do some operations here
std::vector<int>::iterator it = // 查找元素的位置
std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998); // 插入元素
很明显,在3-4行的查找操作没有使用迭代器改变容器元素的值,使用const_iterator
是更好的选择。但是,若使用const_iterator
,返回的也是const_iterator
类型,那么第5行的插入操作是不可行的,因为const_iterator
指向的元素是不能更改值的。
如果按照下面的迭代器类型的强制转换,可能会编译错误。
// 这里更好的选择是使用using, 使用typedef是为了说明这是C++98
typedef std::vector<int>::iterator Iter;
typedef std::vector<int>::const_iterator CIter;
std::vector<int> values;
// ... do some operations here
CIter ci = std::find(static_cast<CIter>(values.begin()),// cast
static_cast<CIter>(values.end()), // cast
1983);
// 插入元素的时候进行一次强制类型转换
values.insert(static_cast<Iter>(ci), 1998); // 编译报错
C++98中,由iterator
转向const_iterator
不是仅仅的一个类型转换声明就行,这里会涉及到其他的一些内部复杂的操作,在这里不再赘述。但是,转换成了const_iterator
,情况会变得更加糟糕。在C++98中,插入操作的时候,元素只能由迭代器进行唯一的位置确定,因此const_iterator
是不可能被接受的,即使它使用了类型转换。
实际上,上述代码6-7行在有些编译器上还是不能运行,C++没有一种从const_iterator
到iterator
的转换,即使有,也是一种不能通用的隐式方法,在这里不值得讨论。因此,C++98中,很少使用const_iterator
了。
在C++11及以后的版本中,const_iterator
一般不是直接显式的使用,而是从迭代器成员函数cbegin()
和cend()
中获取。
上述代码的改进形式(注意插入操作的位置):
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int>values{1, 2, 1983, 1984};
// 使用cbegin()和cedn()作为常指针,返回的it不是常指针
auto it = find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1998);
for(int i = 0; i < values.size(); ++i) {
std::cout << values[i] << " ";
}
std::cout << std::endl;
return 0;
}
// 1 2 1998 1983 1984
上述的cbegin()
和cend()
只是在C++标准库里才有的,如果我们使用其他的一些库的容器或者自定义的容器,需要按照下面的方式进行声明:
template<typename C, typename V>
void findAndInsert(C& container,
const V& targetVal,
const V& insertVal) {
using std::cbegin;
using std::cend;
auto it = std::find(cbegin(container),
cend(container),
targetVal);
container.insert(it, insertVal);
}
但是,上述代码只能在C++14及以后的标准中使用,C++11不可使用。C++11的std
命名空间没有加入cbegin()
和cend()
成员。
如果使用C++11的标准的话,可以按照下面的方式进行声明:
template<typename C>
auto cbegin(const C& container)->decltype(std::begin(container)) {
return std::begin(container);
}
上述代码虽然看起来有些奇怪,但是我们可以推导一下实现的过程:C++11中有begin()
成员,可以返回容器的开始地址;而这里传入的参数是一个常引用,因此在返回的时候,还是不能更改内容的;使用decltype
进行一次显式的元素类型的声明,之后可以保证返回推断时候的正确性。
当然,最后来说,在能使用const_iterator
的地方,就尽量不要使用iterator
,C++14对于这些情况的处理优于C++11。
总结:
- 情况允许下,尽量使用
const_iterator
,迭代的时候,尽量使用cbegin()
和cend()
- 为了最大可能的通用代码,尽量使用
begin()
、end()
等,而不是成员函数的自带的迭代器。
##Item 14:Declare functions noexcept if they won’t emit exceptions.