在 C++ 中将 class 类型映射到其他 class 类型
Mapping class types to other class types in C++
给定以下类型层次结构
class Base { public: virtual ~Base(); }
class OurDervied : public Base {}
class TheirDerived : public Base {}
class General { public: virtual ~General(); }
class MySpecial : public General {};
class YourSpecial : public General {};
我有一个函数f(Base *bp)
。
在f
中,我想创建一个类型取决于传入的类型的对象。例如,f
在接收到[=的实例时创建一个MySpecial
17=],并在收到 TheirDerived
.
的实例时创建 YourSpecial
我想我可以用 dynamic_cast
做到这一点。它可能需要尝试反复转换接收到的对象,直到找到匹配项(返回非 nullptr
)。
另一种选择是给 OurDerived
、TheirDerived
等一个独特的标签,然后使用 switch
case
构造来创建 MySpecial
、YourSpecial
,等等
在 C++ 中是否有任何其他映射 class 类型的选项?
是的,您可以将类型映射委托给派生 类:
class Base
{
public:
virtual General* map() = 0;
};
class OurDerived: public Base
{
protected:
General* map()
{
// compute Type* for OurDerved
}
};
class TheirDerived: public Base
{
protected:
General* map()
{
// compute Type* for TheirDerived
}
};
很难说不知道你的职能有什么职责,或者你对耦合 {My|Your}Special
和 {Our|Their}Derived
有何看法。
基地可以建造吗? Base 或其派生 类 是否允许具有虚拟方法?如果你已经承担了 vtable 的成本,我会将责任委托给派生类型本身,并显式地将方法抽象为 Base 以强制每个派生在这方面进行自我解释。
MySpcial
/ YourSpecial
在类型层次结构中是否相关?否则你最好尝试使用辅助函数的显式模板实例化。
手动类型切换
如果您要创建的类型没有共同的祖先,您别无选择,只能使用
if (dynamic_cast<const DerivedA *>(&base))
{
// Create an object of some type.
}
else if (dynamic_cast<const DerivedB *>(&base))
{
// Create an object of some other type.
}
else if (dynamic_cast<const DerivedC *>(&base))
{
// Create an object of yet aother type.
}
else
{
// Handle the case that no type was matched. Maybe use a default or
// issue an error.
}
级联并且没有直接的方法可以 return 创建的对象,因为函数无法在 运行 时间决定它想要的 return 类型。唯一的出路是使用 type erasure 或丑陋的 union
s.
使用工厂函数
查找Table
幸运的是,如果您要创建的所有类型都派生自公共基础 class,这不是您必须做的,正如您在评论中指出的那样。在这种情况下,您可以将对象的 typeid
映射到创建适当对象的工厂函数。与 运行 时间多态性一样,这需要堆分配。
void
take_action(const Base& base)
{
using FactoryT = std::function<std::unique_ptr<General>()>;
static const std::map<std::type_index, FactoryT> factories {
{typeid(DerivedA), [](){ return std::make_unique<Special1>(); }},
{typeid(DerivedB), [](){ return std::make_unique<Special2>(); }},
{typeid(DerivedC), [](){ return std::make_unique<Special3>(); }},
};
const auto o_uptr = factories.at(typeid(base))();
// Use the object. It can also be returned.
}
我制作了 std::map<std::type_index, std::function<FactoryT()>>
static
,因此它在整个 运行 程序时间内只创建一次。目前尚不清楚这对您的特定情况是否有益。也许对其进行基准测试。
这是一个完整的工作示例。
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <typeindex>
#include <typeinfo>
struct Base
{
virtual ~Base() = default;
virtual std::string
name() const
{
return "Base";
}
};
std::ostream&
operator<<(std::ostream& os, const Base& base)
{
return os << base.name();
}
template<char Token>
struct Derived : Base
{
virtual std::string
name() const override
{
std::string name {"Derived"};
name += Token;
return name;
}
};
using DerivedA = Derived<'A'>;
using DerivedB = Derived<'B'>;
using DerivedC = Derived<'C'>;
struct General
{
virtual ~General() = default;
virtual std::string
name() const
{
return "General";
}
};
template<char Token>
struct Special : General
{
virtual std::string
name() const override
{
std::string name {"Special"};
name += Token;
return name;
}
};
std::ostream&
operator<<(std::ostream& os, const General& general)
{
return os << general.name();
}
using Special1 = Special<'1'>;
using Special2 = Special<'2'>;
using Special3 = Special<'3'>;
void
take_action(const Base& base)
{
using FactoryT = std::function<std::unique_ptr<General>()>;
static const std::map<std::type_index, FactoryT> factories {
{typeid(DerivedA), [](){ return std::make_unique<Special1>(); }},
{typeid(DerivedB), [](){ return std::make_unique<Special2>(); }},
{typeid(DerivedC), [](){ return std::make_unique<Special3>(); }},
};
const auto o_uptr = factories.at(typeid(base))();
std::cout << base << " was mapped to " << *o_uptr << std::endl;
}
int
main()
{
take_action(DerivedA {});
take_action(DerivedB {});
take_action(DerivedC {});
return 0;
}
输出:
DerivedA was mapped to Special1
DerivedB was mapped to Special2
DerivedC was mapped to Special3
访客模式
当然,你应该问问自己为什么你真的想这样做。肯定有这种技术的合法应用,但采用抽象类型然后根据其动态类型采取行动通常是过度抽象的标志,并且会导致代码难以维护。有没有考虑直接把工厂加到Base
?
struct Base
{
virtual ~Base() = default;
virtual std::unique_ptr<General>
getDealer() = 0;
// ...
};
然后 Derived
classes 可以覆盖 getDealer
来执行上述示例中工厂 lambda 所做的事情。
如果这看起来很麻烦(也许 Base
class 根本不应该了解 General
class),您可以考虑使用 visitor pattern。这是一个多一点的工作,但允许更好的解耦。关于这个模式有很多可用的信息,所以我只会展示它在您的特定问题上的应用,如果您需要更多解释,请参考您最喜欢的搜索引擎。
#include <iostream>
#include <memory>
#include <string>
struct BaseVisitor;
struct Base
{
virtual ~Base() = default;
virtual void
accept(BaseVisitor&) const = 0;
virtual std::string
name() const
{
return "Base";
}
};
std::ostream&
operator<<(std::ostream& os, const Base& base)
{
return os << base.name();
}
template<char Token>
struct Derived : Base
{
virtual void
accept(BaseVisitor& vtor) const override;
virtual std::string
name() const override
{
std::string name {"Derived"};
name += Token;
return name;
}
};
using DerivedA = Derived<'A'>;
using DerivedB = Derived<'B'>;
using DerivedC = Derived<'C'>;
struct BaseVisitor
{
virtual ~BaseVisitor() = default;
virtual void
visit(const DerivedA&) = 0;
virtual void
visit(const DerivedB&) = 0;
virtual void
visit(const DerivedC&) = 0;
};
// Cannot be defined earlier because we need the complete type of BaseVisitor.
template<char Token>
void
Derived<Token>::accept(BaseVisitor& vtor) const
{
vtor.visit(*this);
}
struct General
{
virtual ~General() = default;
virtual std::string
name() const
{
return "General";
}
};
template<char Token>
struct Special : General
{
virtual std::string
name() const override
{
std::string name {"Special"};
name += Token;
return name;
}
};
std::ostream&
operator<<(std::ostream& os, const General& general)
{
return os << general.name();
}
using Special1 = Special<'1'>;
using Special2 = Special<'2'>;
using Special3 = Special<'3'>;
void
take_action(const Base& base)
{
struct Mapper : BaseVisitor
{
std::unique_ptr<General> uptr {};
virtual void
visit(const DerivedA&) override
{
this->uptr.reset(new Special1 {});
}
virtual void
visit(const DerivedB&) override
{
this->uptr.reset(new Special2 {});
}
virtual void
visit(const DerivedC&) override
{
this->uptr.reset(new Special3 {});
}
};
Mapper visitor {};
base.accept(visitor);
std::cout << base << " was mapped to " << *visitor.uptr << std::endl;
}
int
main()
{
take_action(DerivedA {});
take_action(DerivedB {});
take_action(DerivedC {});
return 0;
}
请注意我们如何很好地打破 Base
和 General
之间的耦合。不利的一面是,我们不得不通过 BaseVisitor
class.
引入某种父子依赖关系
此解决方案还完全摆脱了任何显式 运行 时间类型推断,并优雅地让动态调度机制在幕后完成所有魔术。
给定以下类型层次结构
class Base { public: virtual ~Base(); }
class OurDervied : public Base {}
class TheirDerived : public Base {}
class General { public: virtual ~General(); }
class MySpecial : public General {};
class YourSpecial : public General {};
我有一个函数f(Base *bp)
。
在f
中,我想创建一个类型取决于传入的类型的对象。例如,f
在接收到[=的实例时创建一个MySpecial
17=],并在收到 TheirDerived
.
YourSpecial
我想我可以用 dynamic_cast
做到这一点。它可能需要尝试反复转换接收到的对象,直到找到匹配项(返回非 nullptr
)。
另一种选择是给 OurDerived
、TheirDerived
等一个独特的标签,然后使用 switch
case
构造来创建 MySpecial
、YourSpecial
,等等
在 C++ 中是否有任何其他映射 class 类型的选项?
是的,您可以将类型映射委托给派生 类:
class Base
{
public:
virtual General* map() = 0;
};
class OurDerived: public Base
{
protected:
General* map()
{
// compute Type* for OurDerved
}
};
class TheirDerived: public Base
{
protected:
General* map()
{
// compute Type* for TheirDerived
}
};
很难说不知道你的职能有什么职责,或者你对耦合 {My|Your}Special
和 {Our|Their}Derived
有何看法。
基地可以建造吗? Base 或其派生 类 是否允许具有虚拟方法?如果你已经承担了 vtable 的成本,我会将责任委托给派生类型本身,并显式地将方法抽象为 Base 以强制每个派生在这方面进行自我解释。
MySpcial
/ YourSpecial
在类型层次结构中是否相关?否则你最好尝试使用辅助函数的显式模板实例化。
手动类型切换
如果您要创建的类型没有共同的祖先,您别无选择,只能使用
if (dynamic_cast<const DerivedA *>(&base))
{
// Create an object of some type.
}
else if (dynamic_cast<const DerivedB *>(&base))
{
// Create an object of some other type.
}
else if (dynamic_cast<const DerivedC *>(&base))
{
// Create an object of yet aother type.
}
else
{
// Handle the case that no type was matched. Maybe use a default or
// issue an error.
}
级联并且没有直接的方法可以 return 创建的对象,因为函数无法在 运行 时间决定它想要的 return 类型。唯一的出路是使用 type erasure 或丑陋的 union
s.
使用工厂函数
查找Table幸运的是,如果您要创建的所有类型都派生自公共基础 class,这不是您必须做的,正如您在评论中指出的那样。在这种情况下,您可以将对象的 typeid
映射到创建适当对象的工厂函数。与 运行 时间多态性一样,这需要堆分配。
void
take_action(const Base& base)
{
using FactoryT = std::function<std::unique_ptr<General>()>;
static const std::map<std::type_index, FactoryT> factories {
{typeid(DerivedA), [](){ return std::make_unique<Special1>(); }},
{typeid(DerivedB), [](){ return std::make_unique<Special2>(); }},
{typeid(DerivedC), [](){ return std::make_unique<Special3>(); }},
};
const auto o_uptr = factories.at(typeid(base))();
// Use the object. It can also be returned.
}
我制作了 std::map<std::type_index, std::function<FactoryT()>>
static
,因此它在整个 运行 程序时间内只创建一次。目前尚不清楚这对您的特定情况是否有益。也许对其进行基准测试。
这是一个完整的工作示例。
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <typeindex>
#include <typeinfo>
struct Base
{
virtual ~Base() = default;
virtual std::string
name() const
{
return "Base";
}
};
std::ostream&
operator<<(std::ostream& os, const Base& base)
{
return os << base.name();
}
template<char Token>
struct Derived : Base
{
virtual std::string
name() const override
{
std::string name {"Derived"};
name += Token;
return name;
}
};
using DerivedA = Derived<'A'>;
using DerivedB = Derived<'B'>;
using DerivedC = Derived<'C'>;
struct General
{
virtual ~General() = default;
virtual std::string
name() const
{
return "General";
}
};
template<char Token>
struct Special : General
{
virtual std::string
name() const override
{
std::string name {"Special"};
name += Token;
return name;
}
};
std::ostream&
operator<<(std::ostream& os, const General& general)
{
return os << general.name();
}
using Special1 = Special<'1'>;
using Special2 = Special<'2'>;
using Special3 = Special<'3'>;
void
take_action(const Base& base)
{
using FactoryT = std::function<std::unique_ptr<General>()>;
static const std::map<std::type_index, FactoryT> factories {
{typeid(DerivedA), [](){ return std::make_unique<Special1>(); }},
{typeid(DerivedB), [](){ return std::make_unique<Special2>(); }},
{typeid(DerivedC), [](){ return std::make_unique<Special3>(); }},
};
const auto o_uptr = factories.at(typeid(base))();
std::cout << base << " was mapped to " << *o_uptr << std::endl;
}
int
main()
{
take_action(DerivedA {});
take_action(DerivedB {});
take_action(DerivedC {});
return 0;
}
输出:
DerivedA was mapped to Special1
DerivedB was mapped to Special2
DerivedC was mapped to Special3
访客模式
当然,你应该问问自己为什么你真的想这样做。肯定有这种技术的合法应用,但采用抽象类型然后根据其动态类型采取行动通常是过度抽象的标志,并且会导致代码难以维护。有没有考虑直接把工厂加到Base
?
struct Base
{
virtual ~Base() = default;
virtual std::unique_ptr<General>
getDealer() = 0;
// ...
};
然后 Derived
classes 可以覆盖 getDealer
来执行上述示例中工厂 lambda 所做的事情。
如果这看起来很麻烦(也许 Base
class 根本不应该了解 General
class),您可以考虑使用 visitor pattern。这是一个多一点的工作,但允许更好的解耦。关于这个模式有很多可用的信息,所以我只会展示它在您的特定问题上的应用,如果您需要更多解释,请参考您最喜欢的搜索引擎。
#include <iostream>
#include <memory>
#include <string>
struct BaseVisitor;
struct Base
{
virtual ~Base() = default;
virtual void
accept(BaseVisitor&) const = 0;
virtual std::string
name() const
{
return "Base";
}
};
std::ostream&
operator<<(std::ostream& os, const Base& base)
{
return os << base.name();
}
template<char Token>
struct Derived : Base
{
virtual void
accept(BaseVisitor& vtor) const override;
virtual std::string
name() const override
{
std::string name {"Derived"};
name += Token;
return name;
}
};
using DerivedA = Derived<'A'>;
using DerivedB = Derived<'B'>;
using DerivedC = Derived<'C'>;
struct BaseVisitor
{
virtual ~BaseVisitor() = default;
virtual void
visit(const DerivedA&) = 0;
virtual void
visit(const DerivedB&) = 0;
virtual void
visit(const DerivedC&) = 0;
};
// Cannot be defined earlier because we need the complete type of BaseVisitor.
template<char Token>
void
Derived<Token>::accept(BaseVisitor& vtor) const
{
vtor.visit(*this);
}
struct General
{
virtual ~General() = default;
virtual std::string
name() const
{
return "General";
}
};
template<char Token>
struct Special : General
{
virtual std::string
name() const override
{
std::string name {"Special"};
name += Token;
return name;
}
};
std::ostream&
operator<<(std::ostream& os, const General& general)
{
return os << general.name();
}
using Special1 = Special<'1'>;
using Special2 = Special<'2'>;
using Special3 = Special<'3'>;
void
take_action(const Base& base)
{
struct Mapper : BaseVisitor
{
std::unique_ptr<General> uptr {};
virtual void
visit(const DerivedA&) override
{
this->uptr.reset(new Special1 {});
}
virtual void
visit(const DerivedB&) override
{
this->uptr.reset(new Special2 {});
}
virtual void
visit(const DerivedC&) override
{
this->uptr.reset(new Special3 {});
}
};
Mapper visitor {};
base.accept(visitor);
std::cout << base << " was mapped to " << *visitor.uptr << std::endl;
}
int
main()
{
take_action(DerivedA {});
take_action(DerivedB {});
take_action(DerivedC {});
return 0;
}
请注意我们如何很好地打破 Base
和 General
之间的耦合。不利的一面是,我们不得不通过 BaseVisitor
class.
此解决方案还完全摆脱了任何显式 运行 时间类型推断,并优雅地让动态调度机制在幕后完成所有魔术。