第五章,技术

31-让函数根据一个以上对象类型来决定如何虚化

问题的本质:单分发的局限性

C++的虚函数机制是”单分发”(Single Dispatch)——仅根据调用对象的动态类型(this指针所指对象)来决定调用哪个虚函数。当函数行为需要同时依赖两个或以上对象的动态类型时,传统虚函数机制无法直接解决。

// 问题场景:游戏对象碰撞检测
class GameObject;
class SpaceShip;
class SpaceStation;
class Asteroid;

// 传统虚函数只能基于this对象类型分发,无法同时根据other的类型决定行为
class GameObject {
public:
virtual void collide(GameObject& other) = 0;
// 无法直接实现:if (this是SpaceShip && other是Asteroid) 做A行为
// if (this是SpaceShip && other是SpaceStation) 做B行为
};

问题的本质:单分发的局限性

通过两次虚函数调用,将类型判断分派给两个对象共同完成。第一次分发确定第一个对象类型,第二次分发确定第二个对象类型。

class SpaceShip;
class SpaceStation;
class Asteroid;

class GameObject {
public:
virtual ~GameObject() = default;
// 第一次分发:由this对象发起
virtual void collide(GameObject& other) = 0;

// 第二次分发:由other对象回调,确定具体类型组合
virtual void collideWith(SpaceShip& ship) = 0;
virtual void collideWith(SpaceStation& station) = 0;
virtual void collideWith(Asteroid& asteroid) = 0;
};

class SpaceShip : public GameObject {
public:
// 第一次分发:将自身作为参数传递给other,让other知道"我"是SpaceShip
void collide(GameObject& other) override {
other.collideWith(*this); // 关键:*this的静态类型是SpaceShip&
}

// 第二次分发:现在明确知道对方类型,执行具体逻辑
void collideWith(SpaceShip& ship) override {
std::cout << "SpaceShip-SpaceShip collision\n";
}
void collideWith(SpaceStation& station) override {
std::cout << "SpaceShip docks with SpaceStation\n";
}
void collideWith(Asteroid& asteroid) override {
std::cout << "SpaceShip destroyed by Asteroid\n";
}
};

class SpaceStation : public GameObject {
public:
void collide(GameObject& other) override {
other.collideWith(*this);
}
void collideWith(SpaceShip& ship) override {
std::cout << "SpaceStation accepts docking from SpaceShip\n";
}
void collideWith(SpaceStation& station) override {
std::cout << "SpaceStation-SpaceStation collision\n";
}
void collideWith(Asteroid& asteroid) override {
std::cout << "SpaceStation damaged by Asteroid\n";
}
};

// 使用示例
SpaceShip ship;
SpaceStation station;
Asteroid asteroid;

ship.collide(station); // 输出: SpaceShip docks with SpaceStation
station.collide(ship); // 输出: SpaceStation accepts docking from SpaceShip
ship.collide(asteroid); // 输出: SpaceShip destroyed by Asteroid

双重分发的局限:每当添加新类型,必须修改所有现有类,添加对应的collideWith方法,违反开闭原则。

解决方案二:基于映射表的分发(Map-Based Dispatch)

使用类型信息作为Key,函数指针或std::function作为Value,将分发逻辑与类型解耦。

#include <typeindex>
#include <map>
#include <functional>
#include <utility>

class GameObject;

// 碰撞处理函数类型
using CollisionHandler = std::function<void(GameObject&, GameObject&)>;

class CollisionMap {
public:
using Key = std::pair<std::type_index, std::type_index>;

// 注册碰撞处理函数(自动处理对称性)
static void addHandler(const std::type_info& type1,
const std::type_info& type2,
CollisionHandler handler) {
auto key = std::make_pair(std::type_index(type1), std::type_index(type2));
handlers_[key] = handler;
// 对称碰撞:A撞B 等同于 B撞A
auto symmetricKey = std::make_pair(std::type_index(type2), std::type_index(type1));
handlers_[symmetricKey] = [handler](GameObject& a, GameObject& b) {
handler(b, a); // 交换参数顺序
};
}

static CollisionHandler getHandler(const std::type_info& type1,
const std::type_info& type2) {
auto key = std::make_pair(std::type_index(type1), std::type_index(type2));
auto it = handlers_.find(key);
return (it != handlers_.end()) ? it->second : nullptr;
}

private:
static std::map<Key, CollisionHandler> handlers_;
};

std::map<CollisionMap::Key, CollisionHandler> CollisionMap::handlers_;

// 简化版游戏对象
class GameObject {
public:
virtual ~GameObject() = default;
virtual const std::type_info& getType() const = 0;

void collide(GameObject& other) {
auto handler = CollisionMap::getHandler(typeid(*this), typeid(other));
if (handler) {
handler(*this, other);
} else {
std::cout << "No collision handler for " << typeid(*this).name()
<< " vs " << typeid(other).name() << "\n";
}
}
};

class SpaceShip : public GameObject {
public:
const std::type_info& getType() const override { return typeid(SpaceShip); }
};

class SpaceStation : public GameObject {
public:
const std::type_info& getType() const override { return typeid(SpaceStation); }
};

class Asteroid : public GameObject {
public:
const std::type_info& getType() const override { return typeid(Asteroid); }
};

// 具体碰撞处理函数
void handleShipStation(GameObject& ship, GameObject& station) {
std::cout << "Ship docking with station\n";
}

void handleShipAsteroid(GameObject& ship, GameObject& asteroid) {
std::cout << "Ship destroyed by asteroid!\n";
}

// 注册器:在main之前自动注册处理函数
class CollisionRegistrar {
public:
CollisionRegistrar() {
CollisionMap::addHandler(typeid(SpaceShip), typeid(SpaceStation), handleShipStation);
CollisionMap::addHandler(typeid(SpaceShip), typeid(Asteroid), handleShipAsteroid);
}
};
static CollisionRegistrar registrar; // 全局对象,程序启动时构造

// 使用示例
SpaceShip ship;
SpaceStation station;
Asteroid asteroid;

ship.collide(station); // Ship docking with station
ship.collide(asteroid); // Ship destroyed by asteroid!

解决方案三:Visitor模式

将操作(碰撞处理)封装为访问者,与对象结构(游戏对象)分离,适合操作经常变化但类型稳定的场景。

// 前向声明
class SpaceShip;
class SpaceStation;
class Asteroid;

// 访问者接口:定义对每个元素类型的访问操作
class GameObjectVisitor {
public:
virtual ~GameObjectVisitor() = default;
virtual void visit(SpaceShip& ship) = 0;
virtual void visit(SpaceStation& station) = 0;
virtual void visit(Asteroid& asteroid) = 0;
};

// 元素接口:接受访问者
class GameObject {
public:
virtual ~GameObject() = default;
virtual void accept(GameObjectVisitor& visitor) = 0;
virtual void collide(GameObject& other) = 0;
};

// 具体元素
class SpaceShip : public GameObject {
public:
void accept(GameObjectVisitor& visitor) override {
visitor.visit(*this); // 告诉访问者:"我"是SpaceShip
}
void collide(GameObject& other) override;
};

class SpaceStation : public GameObject {
public:
void accept(GameObjectVisitor& visitor) override {
visitor.visit(*this);
}
void collide(GameObject& other) override;
};

class Asteroid : public GameObject {
public:
void accept(GameObjectVisitor& visitor) override {
visitor.visit(*this);
}
void collide(GameObject& other) override;
};

// 碰撞访问者:携带碰撞发起者信息,实现双重分发
class CollisionVisitor : public GameObjectVisitor {
public:
CollisionVisitor(GameObject& initiator) : initiator_(initiator) {}

void visit(SpaceShip& ship) override {
handleCollision(initiator_, ship);
}
void visit(SpaceStation& station) override {
handleCollision(initiator_, station);
}
void visit(Asteroid& asteroid) override {
handleCollision(initiator_, asteroid);
}

private:
// 模板方法处理具体类型组合
template<typename T1, typename T2>
void handleCollision(T1& obj1, T2& obj2) {
// 这里明确知道两个对象的精确类型,可以执行特定逻辑
std::cout << "Collision between " << typeid(T1).name()
<< " and " << typeid(T2).name() << "\n";

// 可以进一步分发到具体处理函数
processCollision(obj1, obj2);
}

// 针对具体类型的重载
void processCollision(SpaceShip&, SpaceStation&) {
std::cout << " -> Ship docking procedure initiated\n";
}
void processCollision(SpaceShip&, Asteroid&) {
std::cout << " -> Critical damage! Ship destroyed\n";
}
void processCollision(SpaceStation&, Asteroid&) {
std::cout << " -> Station shield absorbs impact\n";
}
template<typename T, typename U>
void processCollision(T&, U&) {
std::cout << " -> Generic collision handling\n";
}

GameObject& initiator_;
};

// 实现collide方法(必须在所有类定义完成后)
void SpaceShip::collide(GameObject& other) {
CollisionVisitor visitor(*this);
other.accept(visitor); // 第二次分发
}
void SpaceStation::collide(GameObject& other) {
CollisionVisitor visitor(*this);
other.accept(visitor);
}
void Asteroid::collide(GameObject& other) {
CollisionVisitor visitor(*this);
other.accept(visitor);
}

// 使用示例
SpaceShip ship;
SpaceStation station;
Asteroid asteroid;

ship.collide(station); // Collision between SpaceShip and SpaceStation
// -> Ship docking procedure initiated
ship.collide(asteroid); // Collision between SpaceShip and Asteroid
// -> Critical damage! Ship destroyed
station.collide(asteroid);// Collision between SpaceStation and Asteroid
// -> Station shield absorbs impact

现代C++方案:std::variant与std::visit(C++17)

C++17引入std::variant和std::visit,提供了类型安全、无需继承的多重分发机制,彻底摆脱虚函数和继承层次结构的束缚。

#include <variant>
#include <vector>
#include <string>

// 值语义的游戏对象,无需继承
struct SpaceShip {
std::string name = "Enterprise";
int shield = 100;
void collideWith(const SpaceShip& other) const {
std::cout << name << " collides with " << other.name << " (Ship-Ship)\n";
}
void collideWith(const struct SpaceStation& station) const;
void collideWith(const struct Asteroid& asteroid) const;
};

struct SpaceStation {
std::string name = "ISS";
int dockingPorts = 2;
void collideWith(const SpaceShip& ship) const {
std::cout << name << " docks with " << ship.name << " (Station-Ship)\n";
}
void collideWith(const SpaceStation& other) const {
std::cout << name << " collides with " << other.name << " (Station-Station)\n";
}
void collideWith(const struct Asteroid& asteroid) const;
};

struct Asteroid {
int mass = 1000; // kg
void collideWith(const SpaceShip& ship) const {
std::cout << "Asteroid(mass=" << mass << ") destroys " << ship.name << "\n";
}
void collideWith(const SpaceStation& station) const {
std::cout << "Asteroid(mass=" << mass << ") damages " << station.name << "\n";
}
void collideWith(const Asteroid& other) const {
std::cout << "Asteroid collision! Mass: " << mass << " vs " << other.mass << "\n";
}
};

// 前向声明的实现
void SpaceShip::collideWith(const SpaceStation& station) const {
std::cout << name << " docks at " << station.name << " (Ship-Station)\n";
}
void SpaceShip::collideWith(const Asteroid& asteroid) const {
std::cout << name << " is destroyed by asteroid (mass=" << asteroid.mass << ")\n";
}
void SpaceStation::collideWith(const Asteroid& asteroid) const {
std::cout << name << " deflects asteroid (mass=" << asteroid.mass << ")\n";
}

// 使用variant包装所有类型
using GameObject = std::variant<SpaceShip, SpaceStation, Asteroid>;

// 访问器:处理所有类型组合
struct CollisionHandler {
// 处理任意两个类型的组合(编译时生成所有组合)
template<typename T1, typename T2>
void operator()(const T1& obj1, const T2& obj2) const {
// 统一调用接口:obj1.collideWith(obj2)
obj1.collideWith(obj2);
}
};

// 碰撞处理函数
void processCollision(GameObject& obj1, GameObject& obj2) {
// std::visit自动展开所有类型组合,编译时确定调用路径
std::visit(CollisionHandler{}, obj1, obj2);
}

// 使用示例
void testVariantDispatch() {
GameObject ship = SpaceShip{"Falcon", 80};
GameObject station = SpaceStation{"DeepSpace9", 5};
GameObject asteroid = Asteroid{5000};

processCollision(ship, station); // Falcon docks at DeepSpace9 (Ship-Station)
processCollision(ship, asteroid); // Falcon is destroyed by asteroid (mass=5000)
processCollision(station, asteroid); // DeepSpace9 deflects asteroid (mass=5000)
processCollision(asteroid, ship); // Asteroid(mass=5000) destroys Falcon

// 可存储于同质容器
std::vector<GameObject> objects;
objects.push_back(SpaceShip{"A", 50});
objects.push_back(SpaceStation{"B", 3});
objects.push_back(Asteroid{2000});

// 遍历处理所有碰撞组合
for(size_t i = 0; i < objects.size(); ++i) {
for(size_t j = i+1; j < objects.size(); ++j) {
processCollision(objects[i], objects[j]);
}
}
}

C++20 Concepts与静态多态:编译时分发

C++20引入Concepts,可以约束模板参数,实现编译期的”虚函数”——零运行时开销的静态多态,同时保持类型安全。

#include <concepts>
#include <type_traits>

// 定义碰撞能力概念(Concept)
template<typename T>
concept Collidable = requires(T t, const T& ct) {
{ t.collideWith(ct) } -> std::same_as<void>; // 要求有collideWith方法
};

// CRTP基类:提供静态多态接口
template<typename Derived>
class CollidableObject {
public:
// 非虚函数,编译时解析
template<Collidable Other>
void collide(const Other& other) const {
// static_cast确保编译时知道确切类型
static_cast<const Derived*>(this)->collideWith(other);
}

// 获取类型信息(编译期常量)
static constexpr const char* typeName() {
return Derived::staticTypeName();
}
};

// 具体类型
class SpaceShip : public CollidableObject<SpaceShip> {
public:
static constexpr const char* staticTypeName() { return "SpaceShip"; }

template<Collidable T>
void collideWith(const T& other) const {
std::cout << "SpaceShip collides with " << T::typeName() << "\n";
handleCollision(other); // 进一步分发
}

private:
// 针对具体类型的重载(编译时选择)
void handleCollision(const class SpaceStation& station) const {
std::cout << " -> Initiating docking sequence\n";
}
void handleCollision(const class Asteroid& asteroid) const {
std::cout << " -> Evasive maneuvers failed, taking damage\n";
}
template<typename T>
void handleCollision(const T&) const {
std::cout << " -> Generic collision response\n";
}
};

class SpaceStation : public CollidableObject<SpaceStation> {
public:
static constexpr const char* staticTypeName() { return "SpaceStation"; }

template<Collidable T>
void collideWith(const T& other) const {
std::cout << "SpaceStation hit by " << T::typeName() << "\n";
}
};

class Asteroid : public CollidableObject<Asteroid> {
public:
static constexpr const char* staticTypeName() { return "Asteroid"; }

template<Collidable T>
void collideWith(const T& other) const {
std::cout << "Asteroid impacts " << T::typeName() << "\n";
}
};

// 使用:编译期多态,无vtable开销
void testStaticPolymorphism() {
SpaceShip ship;
SpaceStation station;
Asteroid asteroid;

ship.collide(station); // 编译时确定调用SpaceShip::handleCollision(SpaceStation)
ship.collide(asteroid); // 编译时确定调用SpaceShip::handleCollision(Asteroid)
station.collide(ship); // 编译时确定调用SpaceStation::collideWith<SpaceShip>
}

方案对比与选择

+---------------------+------------------+-------------------------+----------------+---------------------------+
| 方案 | 运行时开销 | 扩展性 | 类型安全 | 适用场景 |
+---------------------+------------------+-------------------------+----------------+---------------------------+
| 双重分发 | 两次虚函数调用 | 差(需修改所有类) | 高 | 类型稳定,简单交互 |
| 映射表分发 | 哈希查找+函数调用| 好(运行时注册) | 中(依赖typeid)| 类型动态注册,插件系统 |
| Visitor模式 | 两次虚函数调用 | 中(新类型需改Visitor) | 高 | 操作频繁变化,类型稳定 |
| std::variant+visit | 无(编译时展开) | 中(需重新编译) | 极高 | 类型封闭集,值语义优先 |
| CRTP+Concepts | 无(完全内联) | 差(编译期固定) | 极高 | 性能关键,类型编译期确定 |
+---------------------+------------------+-------------------------+----------------+---------------------------+

关键原则:若类型集合封闭且追求性能,首选C++17 std::variant或C++20 Concepts;若需运行时扩展性,选择映射表分发;若操作变化频繁,使用Visitor模式。
现代C++技术总结:能否实现"根据多个对象类型虚化"

结论:现代C++不仅能实现,而且提供了比传统虚函数更优的解决方案。

1. C++17 std::variant方案:完全替代基于继承的多态,通过std::visit实现编译期多重分发,零运行时开销,类型安全由编译器保证。适用于类型集合封闭的场景(如游戏对象类型固定)。

2. C++20 Concepts + CRTP方案:实现"约束化静态多态",用模板约束替代虚函数,用static_cast替代vtable查找。通过requires表达式精确控制类型能力,编译期错误提示优于模板元编程的晦涩报错。

3. 运行时多分发方案:结合type_index和std::map,实现真正的运行时多重分发,支持动态加载类型(如Mod系统),但牺牲部分性能。
关键洞察:Scott Meyers在1996年提出的"双重分发"问题,在现代C++中已演进为"编译期多重分发优先"的范式。C++20 Concepts让静态多态具备与动态多态同等的表达能力,同时消除vtable开销。对于必须运行时分发的场景,std::function和type_index的组合提供了类型擦除的优雅方案。
最佳实践建议:新项目优先考虑std::variant(值语义、无内存管理负担);性能敏感代码使用CRTP+Concepts;遗留系统改造可采用Visitor模式逐步迁移。

提炼:现代C++使用std::variant实现编译期的多重分发

struct SpaceShip {
void collideWith(const SpaceStation& s) const {
std::cout << "Ship docks at " << s.name << "\n";
}
void collideWith(const Asteroid& a) const {
std::cout << "Ship destroyed by asteroid\n";
}
};

struct SpaceStation {
std::string name;
};

struct Asteroid {
int mass;
};

// 使用 variant
using GameObject = std::variant<SpaceShip, SpaceStation, Asteroid>;//预声明要判断的类型(存入多种类型)

struct CollisionHandler {
template<typename T1, typename T2>
void operator()(const T1& a, const T2& b) const {
a.collideWith(b); // 这里!
}
};

void process(const GameObject& a, const GameObject& b) {
std::visit(CollisionHandler{}, a, b);//访问实际存的类型(内部获取a,b的实际类型,再调用CollisionHandler{}临时对象的operator()函数)
/*
std::visit(visitor, var1, var2, ...)

1. 获取所有 variant 的当前类型索引
index1 = var1.index(), index2 = var2.index()...

2. 根据索引组合,选择对应的代码分支(由编译器生成,编译期生成所有分支)
switch(index1) {
case 0: // SpaceShip
switch(index2) {
case 0: visitor(get<0>(var1), get<0>(var2)); break; // Ship-Ship
case 1: visitor(get<0>(var1), get<1>(var2)); break; // Ship-Station
...
}
}

3. 调用 visitor.operator()(具体对象...)
或 visitor(具体对象...) // 函数调用语法
*/
}

引申: std::visit是如何实现在编译期间生成variant包含类型的所有switch分支的?

using V = std::variant<A, B, C>;  // 3种类型

std::visit(visitor, v1, v2); // 两个variant

//v1 可能是 A/B/C(3种)
//v2 可能是 A/B/C(3种)
//组合数 = 3 × 3 = 9 种

//编译器生成:
switch(v1.index()) {
case 0: // A
switch(v2.index()) {
case 0: visitor(get<0>(v1), get<0>(v2)); break; // A,A
case 1: visitor(get<0>(v1), get<1>(v2)); break; // A,B
case 2: visitor(get<0>(v1), get<2>(v2)); break; // A,C
}
break;
case 1: // B
... //同样3个分支
case 2: // C
... //同样3个分支
}

//如何做到? 模板元编程 + 编译器内置

//1.std::variant 暴露类型信息
template<typename... Types>
class variant {
public:
constexpr size_t index() const noexcept; // 运行时返回0,1,2...

// 关键:类型列表 Types... 编译期可知
};

//2.标准库使用 模板递归/折叠 生成代码

// 伪代码:展示原理(实际更复杂)
template<typename Visitor, typename... Variants, size_t... Indices>
void visit_impl(Visitor&& vis, Variants&&... vars, std::index_sequence<Indices...>) {
// 展开为:vis(std::get<Indices>(vars)...);
}

// 为每种索引组合生成一个实例
template<size_t I1, size_t I2, /*...*/>
void visit_branch(Visitor& vis, Variants&... vars) {
vis( std::get<I1>(vars), std::get<I2>(vars), ... );
}