模版與繼承


C++ 可以定義類別模版,在繼承時對象也可以使用模版,不過並不鼓勵這種做法,例如,你也許會想要量測某個方法的執行時間,為了可以重用量測用的程式碼,或許會採用這樣的設計:

#include <iostream>
#include <chrono>
using namespace std;

unsigned epoch() {
    return chrono::system_clock::now()
              .time_since_epoch().count();
}

template <typename T>
class Timing : public T {
public:
    using T::T;

    void execute() {
        unsigned begin = epoch();
        T::execute();
        cout << epoch() - begin << endl;
    }
};

class EmptyStringOutputter {
public:
    void execute() {
        for(int i = 0; i < 100000000; i++) {
            cout << "";
        }
    }
};

int main() { 
    Timing<EmptyStringOutputter> emptyStringOutputter;
    emptyStringOutputter.execute();

    return 0;
}

Timing 繼承的對象,必須具有 execute 方法,雖說這樣可以達成目的,然而,模版實例化的 Timing<EmptyStringOutputter> 實際上會像是:

class TimingXXX : public EmptyStringOutputter {
public:
    using EmptyStringOutputter::EmptyStringOutputter;

    void execute() {
        unsigned begin = epoch();
        EmptyStringOutputter::execute();
        cout << epoch() - begin << endl;
    }
};

這就要問了,TimingXXX 是一種 EmptyStringOutputter 嗎?或者 Timing<EmptyStringOutputter> 是一種 EmptyStringOutputter 嗎?Timing<EmptyStringOutputter> 就閱讀上,應該是有一個(has-a)EmptyStringOutputter 吧!實際上這個需求,可以用組合(composite)來達成,而不是使用繼承:

#include <iostream>
#include <chrono>
using namespace std;

unsigned epoch() {
    return chrono::system_clock::now()
              .time_since_epoch().count();
}

template <typename T>
class Timing {
    T &target;
public:
    Timing(T &target) : target(target) {}

    void execute() {
        unsigned begin = epoch();
        this->target.execute();
        cout << epoch() - begin << endl;
    }
};

class EmptyStringOutputter {
public:
    void execute() {
        for(int i = 0; i < 100000000; i++) {
            cout << "";
        }
    }
};

int main() { 
    EmptyStringOutputter emptyStringOutputter;
    Timing<EmptyStringOutputter> timing(emptyStringOutputter);
    timing.execute();

    return 0;
}

這麼一來,Timing<EmptyStringOutputter> 就實作與閱讀上,就都是有一個(has-a)EmptyStringOutputter 了。

結合模版與繼承時比較合理的一個例子,是在想共用某個相同實作之時,例如:

#include <iostream>
using namespace std;

template <typename T>
class Comparable {
public:
    virtual int compareTo(T that) = 0;

    bool lessThan(T that) {
        return compareTo(that) < 0;
    }

    bool lessOrEquals(T that) {
        return compareTo(that) <= 0;
    }

    bool greaterThan(T that) {
        return compareTo(that) > 0;
    }

    bool greaterOrEquals(T that) {
        return compareTo(that) >= 0;
    }    

    bool equals(T that) {
        return compareTo(that) == 0;
    }

    virtual ~Comparable() = default;
};

class Ball : public Comparable<Ball> {
    double radius;

public:
    Ball(double radius) : radius(radius) {}

    int compareTo(Ball that) override {
        return this->radius - that.radius;   
    }
};

int main() { 
    Ball b1(100);
    Ball b2(50);

    cout << b1.greaterThan(b2) << endl;

    return 0;
}

在這個例子中,Comparable 實作了部份用於比較的方法,實際上如何比較物件的狀態並不知道,畢竟不同類別的定義不同,因此以模版參數 T 代表物件類型,並規範了 compareTo 必須由子類實作。

Ball 類別繼承時將模版實例化為 Comparable<Ball>Ball 只要重新定義 compareTo,就可以使用事先實作的 lessThan 等方法了,在這種情況下,Ball 是一種 Comparable<Ball>,也就是這球是一種可比較的球,關係上也比較合理。