第五章,技术 31-让函数根据一个以上对象类型来决定如何虚化 问题的本质:单分发的局限性 C++的虚函数机制是”单分发”(Single Dispatch)——仅根据调用对象的动态类型(this指针所指对象)来决定调用哪个虚函数。当函数行为需要同时依赖两个或以上对象的动态类型时,传统虚函数机制无法直接解决。
class GameObject ;class SpaceShip ;class SpaceStation ; class Asteroid ;class GameObject {public : virtual void collide (GameObject& other) = 0 ; };
问题的本质:单分发的局限性 通过两次虚函数调用,将类型判断分派给两个对象共同完成。第一次分发确定第一个对象类型,第二次分发确定第二个对象类型。
class SpaceShip ;class SpaceStation ;class Asteroid ;class GameObject {public : virtual ~GameObject () = default ; virtual void collide (GameObject& other) = 0 ; virtual void collideWith (SpaceShip& ship) = 0 ; virtual void collideWith (SpaceStation& station) = 0 ; virtual void collideWith (Asteroid& asteroid) = 0 ; }; class SpaceShip : public GameObject {public : void collide (GameObject& other) override { other.collideWith (*this ); } 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); station.collide (ship); ship.collide (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; 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" ; } 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.collide (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 ); } 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_; }; 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); ship.collide (asteroid); station.collide (asteroid);
现代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 ; 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" ; } 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); } }; void processCollision (GameObject& obj1, GameObject& obj2) { std::visit (CollisionHandler{}, obj1, obj2); } void testVariantDispatch () { GameObject ship = SpaceShip{"Falcon" , 80 }; GameObject station = SpaceStation{"DeepSpace9" , 5 }; GameObject asteroid = Asteroid{5000 }; processCollision (ship, station); processCollision (ship, asteroid); processCollision (station, asteroid); processCollision (asteroid, ship); 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> template <typename T>concept Collidable = requires (T t, const T& ct) { { t.collideWith (ct) } -> std::same_as<void >; }; template <typename Derived>class CollidableObject {public : template <Collidable Other> void collide (const Other& other) const { 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" ; } }; void testStaticPolymorphism () { SpaceShip ship; SpaceStation station; Asteroid asteroid; ship.collide (station); ship.collide (asteroid); station.collide (ship); }
方案对比与选择 +---------------------+------------------+-------------------------+----------------+---------------------------+ | 方案 | 运行时开销 | 扩展性 | 类型安全 | 适用场景 | +---------------------+------------------+-------------------------+----------------+---------------------------+ | 双重分发 | 两次虚函数调用 | 差(需修改所有类) | 高 | 类型稳定,简单交互 | | 映射表分发 | 哈希查找+函数调用| 好(运行时注册) | 中(依赖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; }; 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); }
引申: std::visit是如何实现在编译期间生成variant包含类型的所有switch分支的? using V = std::variant<A, B, C>; std::visit (visitor, v1, v2); switch (v1. index ()) { case 0 : switch (v2. index ()) { case 0 : visitor (get <0 >(v1), get <0 >(v2)); break ; case 1 : visitor (get <0 >(v1), get <1 >(v2)); break ; case 2 : visitor (get <0 >(v1), get <2 >(v2)); break ; } break ; case 1 : ... case 2 : ... } template <typename ... Types>class variant {public : constexpr size_t index () const noexcept ; }; template <typename Visitor, typename ... Variants, size_t ... Indices>void visit_impl (Visitor&& vis, Variants&&... vars, std::index_sequence<Indices...>) { } template <size_t I1, size_t I2, >void visit_branch (Visitor& vis, Variants&... vars) { vis ( std::get <I1>(vars), std::get <I2>(vars), ... ); }