Effective Modern C++ 第三章第二节,C++新特性

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。因为编译的时候会先检查函数的属性是publicprivate或者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处理上述情况会比较麻烦。

总结:

  1. 使用deleted function而不是私有的未定义的函数
  2. 任何函数都可以是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中满足重写的条件:

  1. 基类函数必须是需函数
  2. 基类和派生类函数的名称、参数、const必须一致
  3. 返回类型和exception必须一致
  4. 函数的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的函数,不会影响函数的功能。

reference qualifiers的介绍:

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,但是这是一种浪费时间的操作,不如直接把该临时变量Widgetvalue当作右值进行引用处理。使用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();// 调用右值的重载,

总结:

  1. 重载用override说明
  2. 成员函数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_iteratoriterator的转换,即使有,也是一种不能通用的隐式方法,在这里不值得讨论。因此,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。

总结:

  1. 情况允许下,尽量使用const_iterator,迭代的时候,尽量使用cbegin()cend()
  2. 为了最大可能的通用代码,尽量使用begin()end()等,而不是成员函数的自带的迭代器。

##Item 14:Declare functions noexcept if they won’t emit exceptions.

补充内容:C++异常处理机制

第一篇

第二篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值