如果類型 Car 是類型 Vehicle 的子類型(subtype秽誊,Car ≤ Vehicle,可以在任何出現(xiàn) Vehicle 的地方用 Car 代替)课舍,那么關(guān)于 Car 和 Vehicle 的復(fù)雜類型(如 std::vector<Car> 和 std::vector<Vehicle>)之間的關(guān)系如下:
- std::vector<Car> 是 std::vector<Vehicle> 的子類型坛猪,所有出現(xiàn) std::vector<Vehicle> 的地方都可以用 std::vector<Car> 代替,即代替方向一致拇派,則稱之為協(xié)變(covariance)。
- std::vector<Vehicle> 是 std::vector<Car> 的子類型本股,所有出現(xiàn) std::vector<Car> 的地方都可以用 std::vector<Vehicle> 代替攀痊,即代替方向相反桐腌,則稱之為逆變(cotravariance)拄显。
- std::vector<Vehicle> 和 std::vector<Car> 之間沒有關(guān)系,則稱之為不變(invariance)案站。
當(dāng)我們深入模板的時候躬审,協(xié)變和逆變這兩個概念就會經(jīng)常地出現(xiàn)。如果一個語言設(shè)計者要想設(shè)計一個支持參數(shù)化多態(tài)(例如,C++ 中模板承边,Java 和 C# 中泛型)的語言遭殉,那么他就必須在協(xié)變,逆變和不變中做出選擇博助∠瘴郏看下面的例子
class Vehicle { };
class Car : public Vehicle { };
std::vector<Vehicle> vehicles;
std::vector<Car> cars;
vehicles = cars; // ERROR, cars 不能代替 vehicles
cars = vehicles; // ERROR, vehicles 不能代替 cars
上述代碼無法通過編譯,cars 無法代替 vehicles富岳,vehicles 也無法代替 cars蛔糯,在此時表現(xiàn)出來的是不變。
每一次模板被實例化窖式,編譯器都會創(chuàng)建一個全新的類型蚁飒;雖然 cars 和 vehicles 實例化了同一個模板,但是他們是兩個完全不同的類型萝喘,之間沒有任何關(guān)系淮逻。在 C++ 中,兩個沒有關(guān)系的用戶自定義類型默認(rèn)是無法彼此相互賦值的阁簸,但是只要我們定義合適的復(fù)制構(gòu)造函數(shù)或者賦值操作符爬早,就可以實現(xiàn)協(xié)變或者逆變。 std::vector 由于沒有實現(xiàn)這樣的復(fù)制構(gòu)造函數(shù)和賦值操作符启妹,因此表現(xiàn)出來的是不變凸椿。
不變只是其中的一種選擇,其他的選擇未必是錯誤的翅溺。事實上脑漫,對于指針和引用,C++ 選擇了協(xié)變咙崎,例如 Car* 可以賦值給 Vehicle* 优幸,更為準(zhǔn)確地說,由于 Car ≤ Vehicle褪猛,則編譯器允許在 Vehicle* 出現(xiàn)的地方由 Car* 來代替网杆。
template <typename T>
using Pointer = T*;
Pointer<Vehicle> vehicle;
Pointer<Car> car;
vehicle = car; // OK, car 可以代替 vehicle,即在使用 vehicle 的時候?qū)嶋H上是用的是 car
上述代碼中伊滋,我們利用 Pointer 表示指針碳却,這種表示方法是為了讓本文協(xié)調(diào)起來,重點討論處理模板時的情況笑旺。這種表示方法不會造成其他副作用昼浦。
那么,當(dāng)在處理模板時筒主,我們應(yīng)該在模板實例化的類之間選擇怎樣的關(guān)系呢关噪?
- 首選是不變鸟蟹,也就是說,當(dāng) Car 和 Vehicle 的模板實例化之間是沒有任何關(guān)系的使兔。這是 C++ 的默認(rèn)選擇建钥。
- 其次的選擇是協(xié)變,即模板實例化之間的關(guān)系與模板參數(shù)之間的關(guān)系是一致的虐沥。例如熊经,std::shared_ptr,std::unique_ptr 等欲险,為了使他們表現(xiàn)更像普通的指針奈搜,應(yīng)該選擇協(xié)變。這種情況不是 C++ 默認(rèn)的盯荤,我們需要編寫合適的復(fù)制構(gòu)造函數(shù)和賦值操作符馋吗。
- 最后的選擇是逆變,模板實例化之間的關(guān)系與模板參數(shù)之間的關(guān)系是顛倒的秋秤。在接下來的部分將會討論到逆變宏粤。
協(xié)變
通過協(xié)變,模板實例化保留了模板參數(shù)之間的關(guān)系灼卢,即所有出現(xiàn) TEMPLATE<Vehicle> 的地方都可以用 TEMPLATE<Car> 來代替绍哎。
在 C++ 標(biāo)準(zhǔn)庫中有如下常見的例子,
std::shared_ptr<Vehicle> shptr_vehicle;
std::shared_ptr<Car> shptr_car;
shptr_vehicle = shptr_car; // OK鞋真,shptr_car 可以代替 shptr_vehicle
shptr_car = shptr_vehicle; // ERROR
std::unique_ptr<Vehicle> unique_vehicle;
std::unique_ptr<Car> unique_car;
unique_vehicle = std::move(unique_car); // OK
unique_car = std::move(unique_vehicle); // ERROR
可以看得出來崇堰,std::shared_ptr 表現(xiàn)的和普通指針是一樣,子類的指針可以賦值給父類的指針涩咖。
下面是標(biāo)準(zhǔn)庫中常見的模板類型海诲,
Type | Covariant | Contravariant |
---|---|---|
STL containers | No | No |
std::initializer_list<T> | No | No |
std::future<T> | No | No |
std::optional<T> | No | No |
std::shared_ptr<T> | Yes | No |
std::unique_ptr<T> | Yes | No |
std::pair<T, U> | Yes | No |
std::tuple<T, U> | Yes | No |
std::atomic<T> | Yes | No |
std::function<R (T)> | Yes (in return) | Yes (in arguments) |
其中我們需要注意的是,標(biāo)準(zhǔn)庫中的所有容器都是不變的檩互,即使容器包含的是指針特幔,例如 std::vector<Car*> 。
接下來我們討論一下闸昨, std::shared_ptr 是如何實現(xiàn)協(xié)變的蚯斯。
template <typename _Tp>
class shared_ptr
{
public:
// 將賦值構(gòu)造函數(shù)設(shè)置為模板,此時shared_ptr具有了隱式類型轉(zhuǎn)換的能力
// 第二個模板參數(shù)的意義是饵较,檢測這種隱式轉(zhuǎn)換是否合理
// 需要注意的是 _Tp1 和 _Tp 一定是不同的類型拍嵌,即標(biāo)準(zhǔn)庫此外必須另提供正常的復(fù)制構(gòu)造函數(shù)
template <typename _Tp1,
typename = typename std::enable_if<std::is_convertiable<_Tp1*,_Tp*>::value>::type>
shared_ptr(const shared_ptr<_Tp1>& __r) noexcept
// 注意 __r 與本實例是不同的類型,訪問數(shù)據(jù)需要 friend
: _M_ptr(__r._M_ptr) { }
// 賦值操作符
template <typename _Tp1>
shared_ptr& operator=(const shared_ptr<_Tp1> __r) noexcept {
_M_ptr = __r._M_ptr;
return *this;
}
private:
// 盡管因模板參數(shù)不同而產(chǎn)生不同的模板實例循诉,彼此可以相互訪問 _M_ptr
template <typename _Tp1> friend class shared_ptr;
_Tp* _M_ptr;
};
以上是經(jīng)過簡化的 GCC 中 std::shared_ptr 的代碼横辆。之所以能夠?qū)崿F(xiàn)協(xié)變,是因為 std::shared_ptr 這兩個特殊的復(fù)制構(gòu)造函數(shù)和賦值操作符是模板成員函數(shù)(template member function)打洼,也就是說在構(gòu)造或者賦值一個 std::shared_ptr 的時候可以接受其他不同實例化的 std::shared_ptr龄糊,這樣子也就實現(xiàn)了隱式轉(zhuǎn)換逆粹。
但是請注意募疮,在 GCC 的實現(xiàn)中炫惩,賦值操作符沒有復(fù)制構(gòu)造函數(shù)的第二個模板參數(shù),沒有檢查兩個指針是否能夠相互轉(zhuǎn)換阿浓。我對于這一點有疑惑他嚷,目前還沒有明白為什么這么設(shè)計。而在 cppreference 和 boost::shared_ptr 中芭毙,賦值構(gòu)造函數(shù)和賦值操作符都沒有第二個模板參數(shù)筋蓖,但是 boost::shared_ptr 中強(qiáng)調(diào)了,對于復(fù)制構(gòu)造函數(shù)退敦,兩個參數(shù)的指針必須要滿足能夠相互轉(zhuǎn)化的條件粘咖,而對于賦值操作符,卻沒有這樣的強(qiáng)調(diào)侈百。我認(rèn)為瓮下,對兩個指針相互轉(zhuǎn)換的檢測發(fā)生在編譯期,即使沒有檢測钝域,到了最底層依然需要將其中的一個指針賦值給另一個指針讽坏,此時如果兩個指針不兼容,則會發(fā)生編譯期錯誤例证,所以沒有第二個模板參數(shù)依然可行路呜。如果大家有其他的想法,可以在評論區(qū)討論织咧。
到目前為止胀葱,我們討論了對于 Car 和 Vehicle 的一層包裝(std::shared_ptr<Car> 和 std::shared_ptr<Vehicle>),我們還可以看看多層包裝的情況笙蒙,
std::tuple<std::atomic<Vehicle>> tuple_atomic_vehicle;
std::tuple<std::atomic<Car>> tuple_atomic_car;
tuple_atomic_vehicle = tuple_atomic_car; // OK
tuple_atomic_car = tuple_atomic_vehicle; // ERROR
std::tuple<std::atomic<Vehicle*>> tuple_atomic_ptrvehicle;
std::tuple<std::atomic<Car*>> tuple_atomic_ptrcar;
tuple_atomic_ptrvehicle = tuple_atomic_ptrcar; // OK
tuple_atomic_ptrcar = tuple_atomic_ptrvehicle; // ERROR
從上邊的例子中巡社,我們看似很容易得到這樣的結(jié)論:幾個具有協(xié)變特性的模板的復(fù)合依然是協(xié)變的。由于 Car* 與 Vehicle* 之間的關(guān)系是協(xié)變手趣,又由于 std::atomic 滿足協(xié)變特性晌该,則 std::atomic<Car*> 與 std::atomic<Vehicle*> 之間的關(guān)系也是協(xié)變,進(jìn)而 std::tuple<std::atomic<Car*>> 與 std::tuple<std::atomic<Vehicle*>> 之間的關(guān)系也是協(xié)變绿渣。
那么我們看下面的例子朝群,
std::shared_ptr<std::tuple<Vehicle>> shptr_tuple_vehicle;
std::shared_ptr<std::tuple<Car>> shptr_tuple_car;
shptr_tuple_vehicle = shptr_tuple_car; // ERROR, cannot convert ‘std::tuple<Car>* const’ to
// ‘std::tuple<Vehicle>*’
shptr_tuple_car = shptr_tuple_vehicle; // ERROR, cannot convert ‘std::tuple<Vehicle>* const’
// to ‘std::tuple<Car>*’
由于 std::shared_ptr<_Tp> 保存的是 _Tp 的指針 _M_ptr,所以在賦值的時候中符,會在 std::tuple<Car>* 與 std::tuple<Vehicle>* 之間進(jìn)行賦值姜胖,又由于協(xié)變的實現(xiàn)是通過編寫合適的復(fù)制構(gòu)造函數(shù)和賦值操作符,而 std::tuple<Car> 與 std::tuple<Vehicle> 兩者之間并沒有繼承關(guān)系淀散,所以會發(fā)生錯誤右莱。
因此蚜锨,我們在寫多層包裝的時候,一定要小心慢蜓,對于每一個模板都要盡量了解內(nèi)部實現(xiàn)亚再。
逆變
我們假設(shè) TEMPLATE<T> 滿足逆變,則所有出現(xiàn) TEMPLATE<Car> 都可以用 TEMPLATE<Vehicle> 代替晨抡,這樣子的表達(dá)不直觀氛悬,而且很容易發(fā)生運行時錯誤。所以逆變的應(yīng)用范圍十分地有限耘柱。
在介紹逆變應(yīng)用之前如捅,我們先看看 C++ 中的一個的特性:返回值協(xié)變(covariant return types)。
class VehicleFactory
{
public:
virtual Vehicle* create() const { return new Vehicle; }
virtual ~VehicleFactory() = default;
};
class CarFactory : public VehicleFactory
{
public:
Car* create() const override { return new Car; }
};
VehicleFactory::create 的返回值是 Vehicle* 调煎,而 CarFactory::create 的返回值是 Car* 镜遣; CarFactory::create 可以代替 VehicleFactory::create(重寫), Car* 代替 Vehicle* 士袄,代替方向一致悲关,即稱為返回值協(xié)變。
假如我們把函數(shù) create 返回的原生指針改成 std::shared_ptr窖剑,上邊的代碼是否還成立坚洽?
class VehicleFactory
{
public:
virtual std::shared_ptr<Vehicle> create() const { return std::shared_ptr<Vehicle>{ new Vehicle }; }
virtual ~VehicleFactory() = default;
};
class CarFactory : public VehicleFactory
{
public:
// ERROR, invalid covariant return type
std::shared_ptr<Car> create() const override { return std::shared_ptr<Car>{ new Car }; }
};
如上所示,不成立西土。因為 std::shared_ptr 的協(xié)變能力是通過編寫代碼實現(xiàn)的讶舰,當(dāng) std::shared_ptr<Car> 賦值給 std::shared_ptr<Vehicle> 時調(diào)用了復(fù)制構(gòu)造函數(shù)或者賦值操作符,而在這里沒有賦值動作(即沒有函數(shù)可以調(diào)用)需了,編譯器只認(rèn)定語言內(nèi)置的指針和引用具有協(xié)變能力跳昼。
對于重寫的函數(shù),返回值是協(xié)變的肋乍,那參數(shù)呢鹅颊?我們從重寫的角度考慮,CarFactory::create 要接受 VehicleFactory::create 的實參墓造,那么后者簽名中的參數(shù)類型應(yīng)該是前者簽名中參數(shù)類型的子類型(或者相同的類型)堪伍,
class Metal { };
class Iron : public Metal { };
class VehicleFactory
{
public:
virtual Vehicle* create(Iron*) const { return new Vehicle; }
virtual ~VehicleFactory() = default;
};
class CarFactory : public VehicleFactory
{
public:
// ERROR,沒有重寫卻被標(biāo)記為 override
// 即使沒有關(guān)鍵字 override 觅闽,也隱藏了父類的函數(shù) create
Car* create(Metal*) const override { return new Car; }
};
上述寫法是錯誤的帝雇。我們拋開 C++ 對于重寫的限制,就從邏輯上來講蛉拙,VehicleFactory::create 的參數(shù)類型可以是 CarFactory::create 的子類型尸闸。如下圖,
C++ 沒有內(nèi)置的對參數(shù)逆變的支持,那么我們應(yīng)該如何實現(xiàn)呢吮廉?
std::function
看如下的例子苞尝,
template <typename T>
using Sink = std::function<void (T*)>;
Sink<Vehicle> vehicle_sink = [] (Vehicle*) { std::cout << "Got some vehicle\n"; };
Sink<Car> car_sink = vehicle_sink; // OK, vehicle_sink可以代替 car_sink
car_sink(new Car);
vehicle_sink = car_sink; // ERROR
car_sink 是一個接受參數(shù)類型為 Car* 且什么都沒有返回的函數(shù), vehicle_sink 是一個接受參數(shù)類型為 Vehicle* 且什么都沒有返回的函數(shù)宦芦;由于 vehicle_sink 可以代替 car_sink宙址,而且 Car* 可以代替 Vehicle*,代替方向相反踪旷,因此 Sink 實現(xiàn)了參數(shù)逆變曼氛。
std::function 也實現(xiàn)了返回值協(xié)變豁辉,
std::function<Car* (Metal*)> car_factory =
[] (Metal*) {
std::cout << "Got some Metal\n";
return new Car;
};
std::function<Vehicle* (Iron*)> vehicle_factory = car_factory;
Vehicle* some_vehicle = vehicle_factory(new Iron); // OK
而且令野,對于 std::shared_ptr,std::function 的返回值協(xié)變和參數(shù)逆變依然成立徽级,
std::cout << std::boolalpha
<< std::is_convertible<
std::function<std::shared_ptr<Car> (std::shared_ptr<Vehicle>)>,
std::function<std::shared_ptr<Vehicle> (std::shared_ptr<Car>)>>::value
<< '\n'; // OK气破,打印結(jié)果為“ true ”
這是由于 std::function 有特殊的復(fù)制構(gòu)造函數(shù)和賦值操作符。
其他
一些語言中函數(shù)的可變性
參數(shù) | 返回值 | |
---|---|---|
C++ (since 1998), Java (since J2SE 5.0), Scala, D | 不變 | 協(xié)變 |
C# | 不變 | 不變 |
Sather | 逆變 | 協(xié)變 |
Eiffel | 協(xié)變 | 協(xié)變 |
繼承與可變性
我們在討論可變性(variance)的時候餐抢,涉及到的都是子類型(subtype)现使,即如果 Car 是 Vehicle 的子類型,則所有出現(xiàn) Vehicle 的地方都可以用 Car 代替旷痕。而在 C++ 中碳锈,實現(xiàn)子類型的 根源 是繼承,即如果 Car 是 Vehicle 的子類(subclass)欺抗,則 Car 是 Vehicle 的子類型售碳,但是并非所有具有子類型關(guān)系的類就一定具有繼承關(guān)系,例如 std::tuple<Car> 和 std::tuple<Vehicle> 之間就沒有繼承關(guān)系绞呈。
引用與可變性
引用和指針都可以實現(xiàn)多態(tài)贸人,但是我們經(jīng)常使用的是指針而忽略引用的作用,接下來將從可變性的角度探討引用發(fā)揮的作用佃声。
前面我們一直討論的是艺智,對于兩個具有子類型關(guān)系的類,他們的復(fù)雜類型之間可變性圾亏。我們討論了利用復(fù)制構(gòu)造函數(shù)和賦值操作符來實現(xiàn) std::shared_ptr<Car> 代替 std::shared_ptr<Vehicle>十拣,卻沒有討論如何實現(xiàn) Car 代替 Vehicle。
class Vehicle
{
public:
Vehicle() { }
~Vehicle() = default;
Vehicle(const Vehicle&) = default;
Vehicle& operator=(const Vehicle&) { return *this; }
};
class Car : public Vehicle
{
public:
Car() { }
~Car() = default;
Car(const Car&) = default;
Car& operator=(const Car&) { return *this; }
};
Car car { };
Vehicle vehicle = car; // OK志鹃,調(diào)用復(fù)制構(gòu)造函數(shù)夭问, car 可以代替 vehicle
依然利用的是復(fù)制構(gòu)造函數(shù)和賦值操作符。
在最后一行處弄跌,調(diào)用了 Vehicle 的復(fù)制構(gòu)造函數(shù) Vehicle(const Vehicle&) { }
甲喝,而實參是 Vehicle 的子類 Car 的實例 car,這里就用到了引用的多態(tài)性铛只,該性質(zhì)是可變性的基礎(chǔ)埠胖,不是或有或無的糠溜。
參考
[1] Covariance and Contravariance in C++ Standard Library
[2] Covariance and contravariance (computer science)
[3] std::shared_ptr
[4] boost::shared_ptr
[5] More C++ Idioms/Covariant Return Types
[6] Subtyping