C/C++ 的协变和逆变
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:183
- 发布: 2025-06-13 20:49
- 最后更新: 2025-06-13 20:49
概述
如果类型 Car 是类型 Vehicle 的子类型(subtype,Car ≤ Vehicle,可以在任何出现 Vehicle 的地方用 Car 代替),那么关于 Car 和 Vehicle 的复杂类型(如 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 无法代替 vehicles,vehicles 也无法代替 cars,在此时表现出来的是不变。 每一次模板被实例化,编译器都会创建一个全新的类型;虽然 cars 和 vehicles 实例化了同一个模板,但是他们是两个完全不同的类型,之间没有任何关系。在 C++ 中,两个没有关系的用户自定义类型默认是无法彼此相互赋值的,但是只要我们定义合适的复制构造函数或者赋值操作符,就可以实现协变或者逆变。 std::vector 由于没有实现这样的复制构造函数和赋值操作符,因此表现出来的是不变。 不变只是其中的一种选择,其他的选择未必是错误的。事实上,对于指针和引用,C++ 选择了协变,例如 Car* 可以赋值给 Vehicle* ,更为准确地说,由于 Car ≤ Vehicle,则编译器允许在 Vehicle* 出现的地方由 Car* 来代替。
cpp
template <typename T>
using Pointer = T*;
Pointer<Vehicle> vehicle;
Pointer<Car> car;
vehicle = car; // OK, car 可以代替 vehicle,即在使用 vehicle 的时候实际上是用的是 car
上述代码中,我们利用 Pointer 表示指针,这种表示方法是为了让本文协调起来,重点讨论处理模板时的情况。这种表示方法不会造成其他副作用。 那么,当在处理模板时,我们应该在模板实例化的类之间选择怎样的关系呢?
- 首选是不变,也就是说,当 Car 和 Vehicle 的模板实例化之间是没有任何关系的。这是 C++ 的默认选择。
- 其次的选择是协变,即模板实例化之间的关系与模板参数之间的关系是一致的。例如,std::shared_ptr,std::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* ,代替方向一致,即称为返回值协变。
