Skip to content

Instantly share code, notes, and snippets.

@tuoxie007
Created May 31, 2020 06:22
Show Gist options
  • Save tuoxie007/1abee0d9764ccae029a1c5d39c42c39c to your computer and use it in GitHub Desktop.
Save tuoxie007/1abee0d9764ccae029a1c5d39c42c39c to your computer and use it in GitHub Desktop.

explicit

构造函数是用来禁止隐式类型转换的

// 条款3

const

const char * p 表示 (char * p) 整体是const,即内容不可变

char * const p 表示 指针 p 是 const,即指针不可变

Widget * const w 与 const Widget * w 一样相同

区别在于 const 在 * 前还是 * 后

const std::vector::iterator it; // it 本身不可变,例如 it ++ 不可用

std::vector::const_iterator it; // it 所指的值不可变,例如 *it = 12 不可用

const 对象不能调用非 const 成员函数,非 const 对象可以调用 const 成员函数

class Widget {
    string text;
public:
    Widget():
    text("HelloWorld")
    {}
    const char &getChar(size_t idx) const {
        return text[idx];
    }
};
int main(int argc, const char * argv[]) {
    const Widget cw;
    cw.getChar(0);
    Widget w;
    w.getChar(0);
    return 0;
}

若存在重载的 const 成员函数和非 const 成员函数,则 const 对象会调用 const 成员函数的版本,而非 const 对象则调用非 const 成员函数的版本

class Widget {
    string text;
public:
    Widget():
    text("HelloWorld")
    {}
    const char &getChar(size_t idx) const {
        return text[idx];
    }
    char &getChar(size_t idx) {
        return text[idx];
    }
};
int main(int argc, const char * argv[]) {
    const Widget cw;
    cw.getChar(0);
    Widget w;
    w.getChar(0);
    return 0;
}

const 和非 const 对象直接可相互转换

class Widget {
    string text;
public:
    Widget():
    text("HelloWorld")
    {}
    const char &getChar(size_t idx) const {
        return text[idx];
    }
    char &getChar(size_t idx) {
        return const_cast<char&>(static_cast<const Widget&>(*this).getChar(idx));
    }
};

const 成员函数无法修改对象内部的任何 bit,但可通过 mutable 绕过特定的成员属性

class Widget {
    mutable int cap;
public:
    void setCap(int cap) const {
        this->cap = cap;
    }
};

// 条款4

构造函数参数初始化

  • 参数初始化可以写在默认构造函数、普通构造函数、拷贝构造函数中,都行
  • arg(a)调用的是拷贝构造函数,arg()调用的是默认构造函数,总之每隔初始化操作只会调用一次构造函数,不会先default再copy
class Widget {
    string text;
public:
    Widget():
    text("HelloWorld")
    {}
    Widget(Widget& w): text(w.text) {}
};
class InitClass {
    Widget w1;
    Widget w2;
public:
    InitClass():
    w1(),
    w2(w1)
    {}
};

内置类型的初始化也可以是无参的,但值未知,(我实测为0)

class Widget {
    int cap;
public:
    Widget(): cap() {
        cout << cap << endl;
    }
};

🏁为避免麻烦,应总是初始化所有成员 初始化代码的顺序不会影响实际的初始化顺序,总是以成员顺序为准。

不同编译单元内的非 local 静态变量(非函数内的static变量)初始化顺序无法确定,若两者存在依赖关系,可能造成不确定性问题,可以通过单例函数进行规避,即使用 local static 变量(该变量确定性的在函数第一次被调用的时候进行初始化),但这种函数存在线程不安全问题,需要加锁,很麻烦,为避免这种问题,应尽量从程序设计上进行规避。

class FileSystem {
};
FileSystem & getFileSystem() {
  static FileSystem fs;
  return fs;
}

对引用的理解:

引用是无所有权无空值可能的“指针”,总是指向别的对象。

引用不对内存管理负责,那总是别人的事,无权即无责。

成员变量也可以为引用,(我觉得应该少用),使用起来风险较高,可能会指向一个不存在的内存,引起不确定性问题,虽然编译器会做一定程度的检测,但还是有绕过的可能,举例:

class Widget {
    size_t &cap;
public:
    Widget(size_t &cap): cap(cap) {
    }
    size_t getCap() {
        return cap;
    }
};
Widget *getWidget() {
    size_t cap(10);
    Widget *w = new Widget(cap);
    cout << w->getCap() << endl; // 打印:10
    return w;
}
int main(int argc, const char * argv[]) {
    Widget *w = getWidget();
    cout << w->getCap() << endl; // 打印:4301990432(随机值)
    return 0;
}

// 条款7

虚函数

虚析构函数对于继承非常重要,非虚析构函数只会按当前变量类型进行匹配,父类类型指针指向了子类对象,delete 的时候只会析构父类的部分,不过需要特别注意的是,反之则没问题,即子类类型的指针指向了子类的对象,delete 的时候会先调用子类的析构函数再调用父类的析构函数,这也好理解,编译器会尽量做的更智能,本着这个目标,编译器是可以根据子类推算出父类的。

class Brand {
    string name;
public:
    Brand(string &name): name(name) {}
    ~ Brand() {
        cout << "brand is deleted" << endl;
    }
};
class Huawei : public Brand {
public:
    Huawei(string &name): Brand(name) {}
    ~ Huawei() {
        cout << "huawei is deleted" << endl;
    }
};
int main(int argc, const char * argv[]) {
    string name("huawei");
    Huawei *h = new Huawei(name);
    delete h;
    // outout:
    // huawei is deleted
    // brand is deleted
    Brand *b = new Huawei(name);
    delete b;
    // outout:
    // brand is deleted
    Huawei hr(name);
    // outout:
    // huawei is deleted
    // brand is deleted
    return 0;
}

若父类的析构函数不是虚函数,子类不可自行将其声明为虚函数,这为我实测,编译不会报错,运行期会崩溃。此问题对普通成员函数不存在。 C++ 中没有 super 指针,若想调用父类方法,需要指明类型,例如

class Huawei : public Brand {
public:
    Huawei(string &name): Brand(name) {}
    virtual ~ Huawei() {
        cout << "huawei is deleted" << endl;
    }
    string getName() {
        return Brand::getName() + "!";
    }
};

虚函数会增加时间和空间成本,非必须就不要用。另外空间变大不只是浪费的问题,一个纯净的 C++ 类的对象在存储空间上的数据跟 C 的结构体能够保持一致,若加上了虚函数就会多出一个 vptr,无法与 C API 兼容,也无法直接序列化到文件中,例如

class Point {
    int x;
    int y;
public:
    Point(int x, int y): x(x), y(y) {}
    ~ Point() {}
};

纯虚函数,就是没有实现的虚函数,包含了此中函数的类为抽象类,无法被实例化,只能等待子类去完善它,且子类在定义的时候必须把纯虚函数都定义或者再次声明为纯虚函数。

class Brand {
    string name;
public:
    Brand(string &name): name(name) {}
    virtual ~ Brand() {
        cout << "brand is deleted" << endl;
    }
    virtual string getName() = 0;
};
class Huawei : public Brand {
public:
    Huawei(string &name): Brand(name) {}
    virtual ~ Huawei() {
        cout << "huawei is deleted" << endl;
    }
    virtual string getName() = 0;
};
class Nova : public Brand {
public:
    Nova(string &name): Brand(name) {}
    virtual ~ Nova() {
        cout << "Nova is deleted" << endl;
    }
    string getName() {
        return "nova";
    }
};
int main(int argc, const char * argv[]) {
    string name("huawei");
    Brand *h = new Nova(name);
    cout << h->getName() << endl;
    delete h;
    return 0;
}

// 条款9

不要在构造函数或析构函数中调用虚函数,因为如果这发生在父类中,则调用的就是父类的方法,而不是子类的,很反直觉。

class Trade {
public:
    Trade() {
        cout << "Trade created" << endl;
        log();
    }
    virtual void log() {
        cout << "Trade log" << endl;
    }
};
class SaleTrade : public Trade {
public:
    SaleTrade() {
        cout << "SaleTrade created" << endl;
        log();
    }
    void log() {
        cout << "SaleTrade log" << endl;
    }
};
int main(int argc, const char * argv[]) {
    SaleTrade st;
// output:
// Trade created
// Trade log
// SaleTrade created
// SaleTrade log
    return 0;
}

这种问题可以通过设计规避,比如通过构造函数中调用父构造函数来向上传递参数。

class Trade {
public:
    Trade(string msg) {
        cout << "Trade created" << endl;
        log(msg);
    }
    void log(string &msg) {
        cout << msg << endl;
    }
};
class SaleTrade : public Trade {
public:
    SaleTrade(size_t price): Trade(getMsg(price)) {
        cout << "SaleTrade created" << endl;
    }
private:
    static string getMsg(size_t price) {
        return string("Sale ") + to_string(price);
    }
};
int main(int argc, const char * argv[]) {
    SaleTrade st(99);
    return 0;
}

// 条款10

自赋值

operator= 的正确写法是最终返回一个 *this,这是一种约定,不是强制,类似的在 operator+= 等运算符中也适用

Trade &operator=(const Trade &t) {
    return *this;
}
Trade &operator+=(const Trade &t) {
    return *this;
}

//条款11

operator= 若遇到自我赋值就可能产生不可预知的问题,比如

class Trade {
    string *sn;
public:
    Trade(string sn): sn(new string(sn)) {}
    Trade& operator=(const Trade &t) {
        delete sn;
        sn = new string(*t.sn);
        return *this;
    }
};

解决方法1:预检测

class Trade {
    string *sn;
public:
    Trade(string sn): sn(new string(sn)) {}
    Trade& operator=(const Trade &t) {
        if (this == &t)
            return *this;
        delete sn;
        sn = new string(*t.sn);
        return *this;
    }
};

解决方法2:临时变量

class Trade {
    string *sn;
public:
    Trade(string sn): sn(new string(sn)) {}
    Trade& operator=(const Trade &t) {
        auto tmp = sn;
        sn = new string(*t.sn);
        delete tmp;
        return *this;
    }
};

解决方法3:swap

class Trade {
    string *sn;
public:
    Trade(string sn): sn(new string(sn)) {}
    void swap(Trade& rhs) {
        std::swap(sn, rhs.sn);
    }
    Trade& operator=(const Trade &t) {
        Trade tmp(t);
        swap(tmp);
        return *this;
    }
};

解决方法3的改进版:

class Trade {
    string *sn;
public:
    Trade(string sn): sn(new string(sn)) {}
    void swap(Trade& rhs) {
        std::swap(sn, rhs.sn);
    }
    Trade& operator=(Trade t) {
        swap(t);
        return *this;
    }
};

在自定义拷贝过程

在自定义拷贝构造函数和拷贝赋值函数的时候需要小心翼翼的赋值所有的成员,编译器不会帮你检测错误,所有的指责归于你自己,尤其是当新增一个属性的时候不能漏了。

在子类中要注意复制父类的属性,但属性往往是private的,子类无法直接访问,此时可以调用父类的对应方法来实现。

class SaleTrade : public Trade {
    string username;
public:
    SaleTrade(string sn, string username): Trade(sn), username(username) {}
    SaleTrade(const SaleTrade &rhs): Trade(rhs), username(rhs.username) {}
    SaleTrade &operator=(const SaleTrade &rhs) {
        Trade::operator=(rhs);
        username = rhs.username;
        return *this;
    }
};

反向隐式类型转换

之前了解的隐式类型转换式类型 B 有一个接收 A 的构造函数(方案1),那么可以拿 A 当 B 用,现在反过来,也可以在 A 中定义一个自动转成 B 的操作符 operator B()(方案2) 完成类似的功能。

class NJString {
    string content;
public:
//    NJString(string c): content(c) {} // 方案1
    NJString(string& c): content(c) {} // 方案1
    NJString(string&& c): content(c) {} // 方案1
    operator string() { // 方案2
        return content;
    }
};
int main(int argc, const char * argv[]) {
    NJString str("OK"); // 方案1
    cout << string(str) << endl; // 方案2
    return 0;
}

与方案1类似,方案2的返回值类型也可以是 &。不过这种隐式转换可能存在使用风险,因为客户可能并不知晓已经做了转换,于是导致二次delete问题。

值传递与引用传递

尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较髙效,并可避免

切割问题

以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当

判定规则不应该是按类型的大小来判定

左右值规则

右值无法传递给 Type&,可以传给 const Type&

class Store {
    string name;
public:
    Store(const string& name): name(name) {}
};
int main(int argc, const char * argv[]) {
    Store s("Apple"); // OK
    return 0;
}
class Store {
    string name;
public:
    Store(string& name): name(name) {}
};
int main(int argc, const char * argv[]) {
    Store s("Apple"); // ERROR
    return 0;
}

右值可以传递给 T&&,也可以传递给 const T&&,好理解,不举例 左值不可以传递给 T&&,也不可以传递给 const T&&,好理解,不举例

左值(non-const)可以传递给 T&,也可以传递给 const T,好理解,不举例

swap

自定义swap通常为了提高交换效率,但自定义有讲究。

对于非类模板的类,自定义方法为:自定义 swap 成员 + std::swap 特化。

需要特别注意的是,swap 不能抛异常,至于为什么,我理解不深入,大概跟资源释放有关系

class Store {
    string name;
public:
    Store(const string& name): name(name) {}
};
class Product {
    Store *store;
    string name;
public:
    Product(Store *store, const string& name): store(store), name(name) {}
    void swap(Product& p) {
        using std::swap;
        swap(name, p.name);
        swap(store, p.store);
    }
};
namespace std {
    template<>
    void swap<Product>(Product& p1, Product& p2) noexcept(is_nothrow_move_constructible<Product>::value && is_nothrow_move_assignable<Product>::value) {
        p1.swap(p2);
    }
}
int main(int argc, const char * argv[]) {
    Store* s = new Store("Apple");
    Product p1(s, "iPhone X");
    Product p2(s, "iPhone Xs");
    using std::swap;
    swap(p1, p2);
    return 0;
}

对于带自定义类型的类模板 C,自定义方法为:自定义 swap 成员+定义 swap 非成员函数模板(参数为C)。

这样相当于对于每此实例化一个类模板的时候,都同时实例化一个对应类型的 swap 非成员函数,这样可以得到优先定义。

之所以不用函数模板特化是因为函数模板不支持“偏特化”(我理解就是特化一半,或者特化里带着泛化),不过类模板支持“偏特化”(我还没见过)。

namespace Network {
template<typename T> class TaskManager {
    T *task;
public:
    TaskManager(T *task): task(task) {}
    void swap(TaskManager<T> & m) {
        using std::swap;
        swap(task, m.task);
    }
};
class HTTPTask {
    string url;
public:
    HTTPTask(const string& url): url(url) {}
};
template<typename T>
void swap(TaskManager<T>& tm1, TaskManager<T>& tm2) {
    tm1.swap(tm2);
}
};
int main(int argc, const char * argv[]) {
    using namespace Network;
    HTTPTask *task1 = new HTTPTask("http://www.google.com");
    TaskManager<HTTPTask> tm1(task1);
    HTTPTask *task2 = new HTTPTask("http://www.youtube.com");
    TaskManager<HTTPTask> tm2(task2);
    using std::swap;
    swap(tm1, tm2);
    return 0;
}

与此同时,在调用侧需要严格遵守规则:using std::swap + swap(a1, a2) 的调用方式。

这源于编译器的名字查找规则,这样写编译器会优先查找自定义的函数,再查找 std::swap 的特化版本,再实例化 std::swap 函数模板(降级到默认行为)。

设计隐患

变量定义一定会同时伴随着类型构造和析构,所以需要注意变量声明的时机,注意考量构造和赋值动作等开销。

C++ 规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上你的程序能编译通过,则不应该存在运行时类型错误的问题。但类型转换破坏了这种规则,因此应当尽量避免不必要的类型转换,尤其是 dynamic_cast(慢),继承导致的类型转换

优先考虑使用虚函数实现。

另外类型转换还可能导致逻辑错误,例如 static_cast(*derived) 实际上返回的不

是当前对象了,而是当前对象base部分的副本。

class HTTPTask {
    string url;
public:
    HTTPTask(const string& url): url(url) {}
    const string& getUrl() {
        return url;
    }
    void setUrl(const string& url) {
        this->url = url;
    }
};
class HTTPSTask : public HTTPTask {
public:
    HTTPSTask(const string& url): HTTPTask(url) {}
};
int main(int argc, const char * argv[]) {
    using namespace Network;
    {
    // 针对指针进行转型
        HTTPSTask *task = new HTTPSTask("https://www.bing.com");
        HTTPTask *baseTask = static_cast<HTTPTask *>(task);
        baseTask->setUrl("http://www.bing.com");
        cout << task->getUrl() << endl << baseTask->getUrl() << endl;
    }
    {
    // 针对对象进行转型
        HTTPSTask *task = new HTTPSTask("https://www.bing.com");
        static_cast<HTTPTask>(*task).setUrl("http://www.bing.com");
        cout << task->getUrl() << endl;
    }
    return 0;
}

指针类型之间的转型是OK的,虽然会发生偏移,但对象的转型很有问题,实际上已经不是当

前对象,是副本了!!!

谨慎将对象内部引用/指针传出到外面

一来可能破坏读写权限,二来可能造成悬空引用,看例子:

class Point {
    int x;
    int y;
public:
    Point(int x, int y): x(x), y(y) {}
    int getX() {
        return x;
    }
    int getY() {
        return y;
    }
    void setX(int x) {
        this->x = x;
    }
    void setY(int y) {
        this->y = y;
    }
};
struct RectData {
    Point topLeft;
    Point bottomRight;
    RectData(const Point& p1, const Point& p2): topLeft(p1), bottomRight(p2) {}
};
class Rect {
    RectData *data;
public:
    Rect(const Point& p1, const Point& p2) {
        data = new RectData(p1, p2);
    }
    Point& getLeftTop() const {
        return data->topLeft;
    }
    Point& getRightBottom() const {
        return data->bottomRight;
    }
};
int main(int argc, const char * argv[]) {
    Rect r(Point(1,2), Point(3,4));
    Point& p = r.getLeftTop();
    p.setX(9);
    cout << r.getLeftTop().getX() << endl; // 输出:9
    return 0;
}

getLeftTop()的本意是不要修改内部值,但返回了自身内部对象的引用,外面就可以随意修

改了,可以改成来解决

const Point& getLeftTop() const { return data->topLeft; }

再看下悬空问题

class Point {
    int x;
    int y;
public:
    Point(int x, int y): x(x), y(y) {}
    int getX() {
        return x;
    }
    int getY() {
        return y;
    }
};
class Rect {
    Point leftTop;
    Point rightBottom;
public:
    Rect(const Point& p1, const Point& p2): leftTop(p1), rightBottom(p2) {}
    Point& getLeftTop() {
        return leftTop;
    }
    Point& getRightBottom() {
        return rightBottom;
    }
};
Point &getLefTop() {
    Rect r(Point(1,2), Point(3,4));
    return r.getLeftTop();
}
int main(int argc, const char * argv[]) {
    Point &p = getLefTop();
    cout << p.getX() << ", " << p.getY() << endl;
    return 0;
}

不小心返回了栈上数据的引用,后面的行为就不可知了。

编译效率优化

通过使用 ref 或者 指针可降低编译依赖程度,指针好理解,实际上 ref 也有这样的特性,即编译器不需要知道 Class 的成员分布,仅仅一个 class FooBar; 这样的定义即可完成编译(前提是你没有访问 ref 对象具体的成员),实验例子:

Light.h

class Light {
    int luminance;
public:
    Light(int luminance): luminance(luminance) {}
    int getLuminance() const {
        return luminance;
    }
};

Helper.h

class Light;
Light &theLight();
void printLight(const Light &light);

Helper.cpp

#include "Helper.hpp"
#include "Light.hpp"
#include <iostream>
Light &theLight() {
    Light *l = new Light(1000);
    return *l;
}
void printLight(const Light &light) {
    std::cout << light.getLuminance() << std::endl;
}

main.cpp

#include "Helper.hpp"
int main(int argc, const char * argv[]) {
    Light &light = theLight();
    printLight(light);
    return 0;
}

main.cpp 文件,其中引用到的 Helper.hpp 并没有 #include "Light.h",仅仅靠 class Light; 即可,这与 ObjC 的 @class 很像,不过这种用法居然可以用在 ref 上,想来是有道理的,只是之前没有特别注意到。

通常为了降低编译依赖度,可以有两种方式设计类:

  • 一种是声明式头文件和定义式头文件分离,分离并不是把一个 class 拆到两个地方去定义,这是无法做到的,而是拆成两个类,例如 Person 类持有一个 PersonImpl 的指针,Person 里只声明一些成员函数却不做具体的工作,而实际的逻辑都写在 PersonImpl 中。这种类通常成为 Handle Classes,也就是设计模式中的组合模式。
  • 另一种是拆成接口类和实现类,再结合工厂函数,客户只需要调用工厂函数产生基类的指针或者引用即可,无需关心背后的实现类。

继承导致的名称覆盖问题

子类若定义了同名的成员函数,会覆盖基类中所有的重载函数(同名不同参的函数版本)

class Person {
public:
    void eat() {
        cout << "eat" << endl;
    }
    void eat(string food) {
        cout << "eat " << food << endl;
    }
};
class Student : public Person {
public:
    void eat() {
        cout << "student eat " << endl;
    }
};
int main(int argc, const char * argv[]) {
    Student s;
    s.eat(); // output: student eat
    s.eat("fruite"); // Compile Error
    return 0;
}

解决办法是使用 using

class Student : public Person {
public:
    using Person::eat; // 基类中所有的 eat 都可以见了
    void eat() {
        cout << "student eat " << endl;
    }
};

纯虚函数

纯虚函数竟然也可以提供实现!只是外部调用的时候需要加上类名作为前缀,如下

Student s;
Person &p = s;
p.Person::run();

即便变量类型是父类也不行,这也好理解,因为是虚函数嘛,所以你不指明具体是哪个类函数,那肯定就调用到了运行时的真实类型上去了。

子类可以公开基类的成员方法

class Database {
    string url;
    bool connected;
public:
    Database(const string& url): url(url) {}
    bool isConnected() const {
        return connected;
    }
private:
    virtual void setConnected(bool connected) {
        this->connected = connected;
    }
};
class MySQLDB : public Database {
public:
    MySQLDB(const string& url): Database(url) {}
    virtual void setConnected(bool connected) {
    }
};
int main(int argc, const char * argv[]) {
    MySQLDB mdb("");
    mdb.setConnected(true);
    return 0;
}

策略模式

策略模式作为继承模式的另一种选择,最原始的是通过子类复写部分虚函数来实现:

class BinaryExpr {
    float lhs;
    float rhs;
public:
    BinaryExpr(float lhs, float rhs): lhs(lhs), rhs(rhs) {}
    float getValue() {
        return doMath(getLhs(), getRhs());
    }
    virtual float doMath(float lhs, float rhs) = 0;
private:
    float getLhs() const {
        return lhs;
    }
    float getRhs() const {
        return rhs;
    }
};
class AddExpr : public BinaryExpr {
public:
    AddExpr(float lhs, float rhs): BinaryExpr(lhs, rhs) {}
    float doMath(float lhs, float rhs) override {
        return lhs + rhs;
    }
};
int main(int argc, const char * argv[]) {
    AddExpr add(3, 2);
    cout << "3 + 2 = " << add.getValue() << endl;
    return 0;
}

第二种方式,用函数指针替代虚函数

class BinaryExpr {
    float lhs;
    float rhs;
public:
    typedef float (*MathFunc)(float, float);
    BinaryExpr(float lhs, float rhs, MathFunc mathFunc): lhs(lhs), rhs(rhs), mathFunc(mathFunc) {}
    float getValue() {
        return mathFunc(getLhs(), getRhs());
    }
private:
    MathFunc mathFunc;
    float getLhs() const {
        return lhs;
    }
    float getRhs() const {
        return rhs;
    }
};
class AddExpr : public BinaryExpr {
public:
    AddExpr(float lhs, float rhs, MathFunc mathFunc): BinaryExpr(lhs, rhs, mathFunc) {}
};
float AddMath(float l, float r) {
    return l + r;
}
int main(int argc, const char * argv[]) {
    AddExpr add(3, 2, AddMath);
    cout << "3 + 2 = " << add.getValue() << endl;
    return 0;
}

第三种是进一步C++式的改造,将函数指针改成函数等价物

class BinaryExpr {
    float lhs;
    float rhs;
public:
    typedef std::function<float (float, float)> MathFunc;
    BinaryExpr(float lhs, float rhs, MathFunc mathFunc): lhs(lhs), rhs(rhs), mathFunc(mathFunc) {}
    float getValue() {
        return mathFunc(getLhs(), getRhs());
    }
private:
    MathFunc mathFunc;
    float getLhs() const {
        return lhs;
    }
    float getRhs() const {
        return rhs;
    }
};
class AddExpr : public BinaryExpr {
public:
    AddExpr(float lhs, float rhs, MathFunc mathFunc): BinaryExpr(lhs, rhs, mathFunc) {}
};
float AddMath(float l, float r) {
    return l + r;
}
class SubExpr : public BinaryExpr {
public:
    SubExpr(float lhs, float rhs, MathFunc mathFunc): BinaryExpr(lhs, rhs, mathFunc) {}
};
struct SubMath {
    float operator()(float l, float r) {
        return l - r;
    }
};
class MulExpr : public BinaryExpr {
public:
    MulExpr(float lhs, float rhs, MathFunc mathFunc): BinaryExpr(lhs, rhs, mathFunc) {}
};
struct MulComputer {
    float doMul(float l, float r) {
        return l * r;
    }
};
int main(int argc, const char * argv[]) {
    AddExpr add(3, 2, AddMath);
    cout << "3 + 2 = " << add.getValue() << endl;
    SubMath subMath;
    SubExpr sub(3, 2, subMath);
    cout << "3 - 2 = " << sub.getValue() << endl;
    MulComputer mulComputer;
    AddExpr::MathFunc mulMath = std::bind(&MulComputer::doMul, &mulComputer, std::placeholders::_1, std::placeholders::_2);
//    auto mulMath = std::bind(&MulComputer::doMul, &mulComputer, std::placeholders::_1, std::placeholders::_2);
    MulExpr mul(3, 2, mulMath);
    cout << "3 * 2 = " << mul.getValue() << endl;
    return 0;
}

这种方式具有极大的延展性,可以看到,这里用了三种方式作为 MathFunc 参数,函数指针、函数对象、functional 绑定对象。 其中 functional 用法比较难以理解 std::bind 的第一个参数可以是C函数也可以是成员函数,后面的参数都是可选的。如果绑定的是成员函数,成员函数可见的参数是两个,但实际上还有一个隐式参数,即 this 对象,这里传的是一个 MulComputer 对象的指针(&mulComputer),其实也可以是对象的值(mulComputer),后续的参数可以是真实的值,也可以是占位符,剩几个参数就传几个占位符,占位符的顺序也可以自有安排(实际的调用顺序会按 _n 来放置),所以非常非常灵活。functional 动态绑定这种方式其实不是必要的,用自定义C函数来做适配器也可以实现,只是略微麻烦,例如你可以写一个函数

float MulFunc(const MulComputer &m, float l, float r) {
    return m.doMul(l, r);
}

参数默认值

函数默认值可以是一个表达式,例如一个构造函数,或者一个new

class C1 {
    int i1;
public:
    C1(int i1=1) {
        this->i1 = i1;
    }
    int gi1() const {
        return i1;
    }
};
class C2 {
    C1 c1;
    C1 p1;
public:
    void f2(const C1& c1=C1(), C1 *p1=new C1(2)) {
        cout << "c1=" << c1.gi1() << endl;
        cout << "p1=" << p1->gi1() << endl;
    }
};

虚函数的参数默认值的绑定规则很反直觉,即所谓的静态绑定,与虚函数本身的动态绑定不一致。于是你用基类的引用/指针去调用虚函数,函数调用的是可能子类的,但默认值却来源于基类的定义!

class B1 {
public:
    virtual void mf(int a=3) {
        cout << "B1::mf a = " << a << endl;
    }
};
class D1 : public B1 {
public:
    virtual void mf(int a=6) {
        cout << "D1::mf a = " << a << endl;
    }
};
int main(int argc, const char * argv[]) {
    B1 *b = new B1();
    B1 *d = new D1();
    b->mf();
    d->mf();
    (static_cast<D1*>(d))->mf();
    return 0;
}

私有继承

私有继承并非为 is-a 设计,其实是聚合模式的另一种选择,私有继承仅继承了其功能,这些功能可以在子类内部使用,而外部无法访问到,并且因此,子类无法当作父类对象而使用

class Computer {
};
class MacBook : private Computer {
};
void useComputer(const Computer& computer) {
}
int main(int argc, const char * argv[]) {
    Computer computer;
    useComputer(computer);
    MacBook mac;
    useComputer(mac); // 编译错误
    return 0;
}

聚合模式之所以需要私有继承作为一种可选项,是因为其在某些情况下具有一定优势,例如经常我们用到一个类作为实现某项功能的手段,但这个类需要一个 delegate 来接收回调,这就需要再额外定义一个 Delegate 的协议类,于是还是免不了继承,很麻烦,用私有继承可以解决这种问题

class Event {
public:
    void click(unsigned times) {
        if (times == 1)
            onClick();
        else if (times == 2)
            onDoubleClick();
    }
    virtual void onClick() = 0;
    virtual void onDoubleClick() = 0;
};
class Button : private Event {
public:
    using Event::click;
private:
    virtual void onClick() {
        cout << "button clicked" << endl;
    }
    virtual void onDoubleClick() {
        cout << "button double clicked" << endl;
    }
};
int main(int argc, const char * argv[]) {
    Button button;
    button.click(2);
    return 0;
}

typename 值得注意之处

一般情况下,typename 和 class 是等价的,但在某些情况下,为了避免编译器理解上的歧义,需要在嵌套从属类型前面 typename,但不包括类型声明语句,即继承列表和成员初始化列表(忘了可以看条款42)。

获取迭代器的值类型可以用 std::iterator_traits::value_type 表示

类模板的继承之诡异

当你继承一个类模板的时候,父类的函数很可能是不能调用的,因为模板有一种特化的情况,特化可以跟模板完全不同,不非得遵守类模板中声明的函数(真是有毒啊!),而编译器倾向于提早报错,而非具像化失败才报,解决方法有三

template <typename T>
class B2 {
public:
    void mf(T &t) {
        t.mf();
    }
};
template <>
class B2<int> {
};
template <typename T>
class D2 : public B2<T> {
public:
    void mf2(T &t) {
        mf(); // 编译错误
    }
};
template <typename T>
class D3 : public B2<T> {
public:
    void mf2(T &t) {
        this->mf(t);
        this->mf_NotExist();
    }
};
template <typename T>
class D4 : public B2<T> {
public:
    using B2<T>::mf;
    using B2<T>::mf_NotExist;
    void mf2(T &t) {
        mf(t);
        mf_NotExist();
    }
};
template <typename T>
class D5 : public B2<T> {
public:
    void mf2(T &t) {
        B2<T>::mf(t);
        B2<T>::mf_NotExist();
    }
};

这三种方法说到底就是告诉编译器,我知道我知道,你先别报错了,就当有,而且这种方法甚至可以欺骗编译器,调用一个完全没有声明的函数。

成员函数模板

成员函数模板对于处理继承关系非常有价值,因为父子类具相化出的模板类之间并无关联,比如只能父子类的智能指针间的相互转换

template<class T>
class AutoPtr {
    T *p;
public:
    AutoPtr(T *p): p(p) {
        cout << "0" << endl;
    }
    AutoPtr(const AutoPtr& ptr): p(ptr.get()) {
        cout << "1" << endl;
    };
    template<class C>
    AutoPtr(const AutoPtr<C>& ptr): p(ptr.get()) {
        cout << "2" << endl;
    }
    T *get() const {
        return p;
    }
};
int main(int argc, const char * argv[]) {
    B1 *bp = new B1();
    AutoPtr<B1> b(bp);
    D1 *dp = new D1();
    AutoPtr<D1> d(dp);
    AutoPtr<B1> b1(d); // 输出 2
    AutoPtr<B1> b2(b); // 输出 1
    return 0;
}

额外需要指出的是泛化拷贝构造函数不会组织编译器合成默认拷贝构造函数,其中输出1覆盖的的就是合成的版本,这个版本表示的是具相化的 AutoPtr 的同类拷贝,而泛化则负责具相化的 AutoPtr 和 AutoPtr 的拷贝

模板的声明和定义一般不可以分开到不同文件

模板具相化发生在调用处,如果分开定义,具相化过程无法获取模板的所有代码,即编译

函数模板推断不会考虑隐式类型转换

template <typename T>
class Number {
    T v;
public:
    Number(T v): v(v) {}
    T getValue() const {
        return v;
    }
};
template <typename T>
Number<T> operator+(const Number<T> &n1, const Number<T> &n2) {
    return Number<T>(n1.getValue() + n2.getValue());
}
int main(int argc, const char * argv[]) {
    Number<int> intNum1(3);
    Number<int> intNum2(5);
    auto sum = intNum1 + intNum2;
    cout << sum.getValue() << endl;
    intNum1 + 5; // 编译失败
    return 0;
}

解决办法是借用类模板声明函数,但函数定义需要放在类模板中,否则无法链接成功,因为没有真正实现具相化

template <typename T>
class Number {
    T v;
public:
    friend Number<T> operator+(const Number<T> &n1, const Number<T> &n2) {
        return Number<T>(n1.getValue() + n2.getValue());
    }
    Number(T v): v(v) {}
    T getValue() const {
        return v;
    }
};
int main(int argc, const char * argv[]) {
    Number<int> intNum1(3);
    auto sum = intNum1 + 5;
    cout << sum.getValue() << endl;
    return 0;
}

iterator_traits的原理

对于迭代器类型:首先模板遵循一种约定,根据这种预定 traits 可以获取到类型信息,这个所谓的信息是一组预定义好的结构体,然后当函数需要基于不同类型做不同逻辑的时候,不是通过运行时的条件控制完成的,而是通过模板特化,利用编译器类型匹配逻辑进行的,当传入不同类型的对象,就能匹配到不同的特化函数,例子可以看条款47

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
然后 doAdvance 有多个特化版本

数组处理继承关系

例如函数参数为 Base[],你硬要塞一个 Derived[] 进去,就会可能问题。

首先数组下标运算依赖编译器推测,如果类型不能精确匹配,步长会搞错。

其次析构函数也可能出现问题,调用了父类的析构函数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment