C/C++ 的协变和逆变

概述

如果类型 Car 是类型 Vehicle 的子类型(subtype,CarVehicle,可以在任何出现 Vehicle 的地方用 Car 代替),那么关于 CarVehicle 的复杂类型(如 std::vector<Car>std::vector<Vehicle>)之间的关系如下:

  • std::vector<Car>std::vector<Vehicle> 的子类型,所有出现 std::vector<Vehicle> 的地方都可以用 std::vector<Car> 代替,即代替方向一致,则称之为协变(covariance)。
  • std::vector<Vehicle>std::vector<Car> 的子类型,所有出现 std::vector<Car> 的地方都可以用 std::vector<Vehicle> 代替,即代替方向相反,则称之为逆变(cotravariance)。
  • std::vector<Vehicle>std::vector<Car> 之间没有关系,则称之为不变(invariance)。

当我们深入模板的时候,协变和逆变这两个概念就会经常地出现。如果一个语言设计者要想设计一个支持参数化多态(例如,C++ 中模板,Java 和 C# 中泛型)的语言,那么他就必须在协变,逆变和不变中做出选择。看下面的例子:

cpp 复制
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 无法代替 vehiclesvehicles 也无法代替 cars,在此时表现出来的是不变。 每一次模板被实例化,编译器都会创建一个全新的类型;虽然 carsvehicles 实例化了同一个模板,但是他们是两个完全不同的类型,之间没有任何关系。在 C++ 中,两个没有关系的用户自定义类型默认是无法彼此相互赋值的,但是只要我们定义合适的复制构造函数或者赋值操作符,就可以实现协变或者逆变。 std::vector 由于没有实现这样的复制构造函数和赋值操作符,因此表现出来的是不变。 不变只是其中的一种选择,其他的选择未必是错误的。事实上,对于指针和引用,C++ 选择了协变,例如 Car* 可以赋值给 Vehicle* ,更为准确地说,由于 CarVehicle,则编译器允许在 Vehicle* 出现的地方由 Car* 来代替。

cpp 复制
template <typename T>
using Pointer = T*;

Pointer<Vehicle> vehicle;
Pointer<Car> car;

vehicle = car; // OK, car 可以代替 vehicle,即在使用 vehicle 的时候实际上是用的是 car

上述代码中,我们利用 Pointer 表示指针,这种表示方法是为了让本文协调起来,重点讨论处理模板时的情况。这种表示方法不会造成其他副作用。 那么,当在处理模板时,我们应该在模板实例化的类之间选择怎样的关系呢?

  • 首选是不变,也就是说,当 CarVehicle 的模板实例化之间是没有任何关系的。这是 C++ 的默认选择。
  • 其次的选择是协变,即模板实例化之间的关系与模板参数之间的关系是一致的。例如,std::shared_ptrstd::unique_ptr 等,为了使他们表现更像普通的指针,应该选择协变。这种情况不是 C++ 默认的,我们需要编写合适的复制构造函数和赋值操作符。
  • 最后的选择是逆变,模板实例化之间的关系与模板参数之间的关系是颠倒的。在接下来的部分将会讨论到逆变。

协变

通过协变,模板实例化保留了模板参数之间的关系,即所有出现 TEMPLATE<Vehicle> 的地方都可以用 TEMPLATE<Car> 来代替。 在 C++ 标准库中有如下常见的例子:

cpp 复制
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 表现的和普通指针是一样,子类的指针可以赋值给父类的指针。

标准库中常见的模板类型:

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)

其中我们需要注意的是,标准库中的所有容器都是不变的,即使容器包含的是指针,例如 std::vector<Car*>

逆变

我们假设 TEMPLATE<T> 满足逆变,则所有出现 TEMPLATE<Car> 都可以用 TEMPLATE<Vehicle> 代替,这样子的表达不直观,而且很容易发生运行时错误。所以逆变的应用范围十分地有限。 在介绍逆变应用之前,我们先看看 C++ 中的一个的特性:返回值协变(covariant return types)。

cpp 复制
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* ,代替方向一致,即称为返回值协变。

协变.webp