从动态类型信息创建新对象

Posted

技术标签:

【中文标题】从动态类型信息创建新对象【英文标题】:Creating a new object from dynamic type info 【发布时间】:2010-01-09 09:05:01 【问题描述】:

在 C++ 中,有没有办法查询对象的类型,然后使用该信息动态创建相同类型的新对象?

例如,假设我有一个简单的 3 类层次结构:

class Base
class Foo : public Base
class Bar : public Base

现在假设我给你一个转换为 Base 类型的对象——它实际上是 Foo 类型。 有没有办法查询类型并使用该信息稍后创建 Foo 类型的新对象?

【问题讨论】:

【参考方案1】:

克隆方法

查询类型并让您从该信息构造的语言没有提供任何东西,但是您可以通过各种方式为您的类层次结构提供功能,其中最简单的是使用虚拟方法:

struct Base 
  virtual ~Base();
  virtual std::auto_ptr<Base> clone(/*desired parameters, if any*/) const = 0;
;

这有点不同:克隆当前对象。这通常是您想要的,并允许您将对象作为模板保留,然后您可以根据需要对其进行克隆和修改。

扩展Tronic,您甚至可以将generate 扩展为clone function。

为什么要auto_ptr?因此,您可以使用 new 来分配对象,使所有权的转移显式化,并且调用者毫无疑问 delete 必须解除分配它。例如:

Base& obj = *ptr_to_some_derived;
 // since you can get a raw pointer, you have not committed to anything
  // except that you might have to type ".release()"
  Base* must_free_me = obj.clone().release();
  delete must_free_me;

 // smart pointer types can automatically work with auto_ptr
  // (of course not all do, you can still use release() for them)
  boost::shared_ptr<Base> p1 (obj.clone());
  auto_ptr<Base>          p2 (obj.clone());
  other_smart_ptr<Base>   p3 (obj.clone().release());

 // automatically clean up temporary clones
  // not needed often, but impossible without returning a smart pointer
  obj.clone()->do_something();

对象工厂

如果您希望完全按照您的要求去做并获得一个可以独立于实例使用的工厂:

struct Factory ; // give this type an ability to make your objects

struct Base 
  virtual ~Base();
  virtual Factory get_factory() const = 0; // implement in each derived class
    // to return a factory that can make the derived class
    // you may want to use a return type of std::auto_ptr<Factory> too, and
    // then use Factory as a base class
;

许多与克隆方法相同的逻辑和功能都可以使用,因为 get_factory 完成了一半的相同角色,而返回类型(及其含义)是唯一的区别。

我还报道过coupletimes 的工厂。您可以调整我的SimpleFactory class,以便您的工厂对象(由 get_factory 返回)持有对全局工厂的引用以及传递给创建的参数(例如,类的注册名称——考虑如何应用 boost::functionboost::bind 使其易于使用)。

【讨论】:

+1,是的。请注意,术语“克隆”不是任意的……它是人们用来描述这种模式的标准,您可以通过搜索“C++ 克隆对象”en.wikipedia.org/wiki/Cloning_(programming) 在 C++ 中创建工厂的一个很好的参考是 Andrei Alexandrescu 的“现代 C++ 设计”。 不,这里很完美(特别是表示使用了 new,所以你知道如果指针被释放则需要 delete),并且它在当前的 C++ 标准库中。你不能对其他任何事情说同样的话(目前)。使用 release() 将指针放入您喜欢的任何其他智能指针类型。 使用 auto_ptr 可以让您在代码的其余部分中使用它(或编写一些不必要的 .release() 内容)。通用类应该返回一个普通指针,该类的用户可以将其存储在他们选择的任何类型的智能指针中。 请将auto_ptr 替换为unique_ptr 或其他合适的名称。【参考方案2】:

通过基类创建对象副本的常用方法是添加一个clone方法,它本质上是一个多态的复制构造函数。这个虚函数通常需要在每个派生类中定义,但您可以使用Curiously Recurring Template Pattern 避免一些复制和粘贴:

// Base class has a pure virtual function for cloning
class Shape 
public:
    virtual ~Shape()  // Polymorphic destructor to allow deletion via Shape*
    virtual Shape* clone() const = 0; // Polymorphic copy constructor
;
// This CRTP class implements clone() for Derived
template <typename Derived> class Shape_CRTP: public Shape 
public:
    Shape* clone() const 
        return new Derived(dynamic_cast<Derived const&>(*this));
    
;
// Every derived class inherits from Shape_CRTP instead of Shape
// Note: clone() needs not to be defined in each
class Square: public Shape_CRTP<Square> ;
class Circle: public Shape_CRTP<Circle> ;
// Now you can clone shapes:
int main() 
    Shape* s = new Square();
    Shape* s2 = s->clone();
    delete s2;
    delete s;

请注意,您可以将相同的 CRTP 类用于每个派生类中相同但需要了解派生类型的任何功能。除了 clone() 之外,还有许多其他用途,例如双重调度。

【讨论】:

好点,但您可以将 dynamic_cast 更改为 static_cast,因为您知道继承。 使用 dynamic_cast 可以捕捉到 Derived 定义不正确(例如通过复制和粘贴)的情况,否则会导致 UB。与分配内存和实际复制对象的损失相比,动态转换的损失可以忽略不计。使用 dynamic_cast 还允许强制转换与无法使用 static_cast 的多重继承一起工作。那些知道自己在做什么的人可以很容易地用 static_cast 替换演员表。 你能构造这样一个UB案例吗? static_cast 与 reinterpret_cast 不同(这很容易导致 UB)。我也无法构造一个 MI 案例,其中 static_cast 不明确但 dynamic_cast 不明确。 三角形类:public Shape_CRTP ; 关于 MI,你是对的,限制不适用于这里,因为不需要在层次结构的分支之间或从虚拟基础进行转换。【参考方案3】:

只有一些 hacky 方法可以做到这一点。

第一个,恕我直言,最丑的是:

Base * newObjectOfSameType( Base * b )

  if( dynamic_cast<Foo*>( b ) ) return new Foo;
  if( dynamic_cast<Bar*>( b ) ) return new Bar;

请注意,这仅在您启用了 RTTI 并且 Base 包含一些虚拟功能时才有效。

第二个更简洁的版本是在基类中添加一个纯虚拟克隆函数

struct Base  virtual Base* clone() const=0; 
struct Foo : public Base  Foo* clone() const  return new Foo(*this); 
struct Bar : public Base  Bar* clone() const  return new Bar(*this); 

Base * newObjectOfSameType( Base * b )

  return b->clone();

这样更整洁。

关于这个的一个很酷/有趣的事情是 Foo::clone 返回 Foo*,而 Bar::clone 返回 Bar*。你可能认为这会破坏一些东西,但由于 C++ 的一个称为协变返回类型的特性,一切正常。

不幸的是,协变返回类型不适用于智能指针,因此使用 sharted_ptrs 您的代码将如下所示。

struct Base  virtual shared_ptr<Base> clone() const=0; 
struct Foo : public Base  shared_ptr<Base> clone() const  return shared_ptr<Base>(new Foo(*this) ); 
struct Bar : public Base  shared_ptr<Base> clone() const  return shared_ptr<Base>(new Bar(*this)); 

shared_ptr<Base> newObjectOfSameType( shared_ptr<Base> b )

  return b->clone();

【讨论】:

注意你必须非常小心你的演员表的顺序!示例:SubFoo(继承 Foo)必须在 Foo 之前。 不是很好的答案。第一个选项只是废话(如果没有匹配项,则代码缺少返回/抛出)。虽然返回 Derived* 是一个不错的技巧,但它几乎没有用处,因为它仍然不会使 Base::clone 返回正确的类型。不过,第二个选项是每个人通常都会做的事情,通常使用 Base* 返回类型。第三个选项是不必要和讨厌的,因为它强制使用 shared_ptrs。可以这样做 shared_ptr<base> ptr(new Derived()); 但最好通过返回 Base*(或 auto_ptr)来避免整个问题。 Tronic:协变返回类型很有用,因为当您已经拥有静态类型信息时,它们会保留。 (尽管正如您所说,当您没有它时它们没有任何好处。)stdlib 中的一个示例是 fstream::rdbuf (但是,它有其自身的问题..)。但是,当返回必须释放的指针时,我更喜欢自我记录,这意味着这种模式需要像 auto_ptr 这样的智能指针类型,而不是协变返回类型。 @Tronic:第一种方法的优点是它是非侵入性的,它适用于一组已知的对象。关于丢失的返回:建议读者理解而不是复制解决方案。 --- 你期望 base::clone 返回什么类型?这种争论几乎和称这些例子“垃圾”或“讨厌”一样一周。 它是非侵入式的。我完全忽略了这一点。关于返回类型的论点与 IMO 非常相关,因为您通常根本不会调用derivedPtr->clone(),而是(几乎)总是通过基指针使用它。由于派生指针会导致智能指针出现问题(返回 auto_ptr,或使用赋值语法来初始化任何智能指针),这似乎不是一个好的解决方案。【参考方案4】:

您可以使用例如typeid查询一个对象的动态类型,但是我不知道有什么方法可以直接从类型信息中实例化一个新对象。

但是,除了上面提到的clone 方法之外,您还可以使用工厂:

#include <typeinfo>
#include <iostream>

class Base

public:
    virtual void foo() const
    
        std::cout << "Base object instantiated." << std::endl;
    
;


class Derived : public Base

public:
    virtual void foo() const
    
        std::cout << "Derived object instantiated." << std::endl;
    
;


class Factory

public:
    static Base* createFrom( const Base* x )
    
        if ( typeid(*x) == typeid(Base) )
        
            return new Base;
        
        else if ( typeid(*x) == typeid(Derived) )
        
            return new Derived;
        
        else
        
            return 0;
        
    
;


int main( int argc, char* argv[] )

    Base* X = new Derived;
    if ( X != 0 )
    
        std::cout << "X says: " << std::endl;
        X->foo();
    

    Base* Y = Factory::createFrom( X );
    if ( Y != 0 )
    
        std::cout << "Y says: " << std::endl;
        Y->foo();
    

    return 0;

P.S.:这个代码示例的基本部分当然是Factory::createFrom 方法。 (这可能不是最漂亮的 C++ 代码,因为我的 C++ 已经有点生疏了。工厂方法可能不应该是静态的,再想一想。)

【讨论】:

【参考方案5】:

我在我的项目中使用宏来综合这些方法。 我现在只是在研究这种方法,所以我可能错了,但这是我的 IAllocable.hh 代码中对您问题的答案。请注意,我使用的是 GCC 4.8,但我希望 4.7 适合。

#define SYNTHESIZE_I_ALLOCABLE \
    public: \
    auto alloc() -> __typeof__(this)  return new (__typeof__(*this))();  \
    IAllocable * __IAllocable_alloc()  return new (__typeof__(*this))();  \
    private:


class IAllocable 
public:
    IAllocable * alloc() 
        return __IAllocable_alloc();
    
protected:
    virtual IAllocable * __IAllocable_alloc() = 0;
;

用法:

class Usage : public virtual IAllocable 

    SYNTHESIZE_I_ALLOCABLE

public:
    void print() 
        printf("Hello, world!\n");
    
;

int main() 
    
        Usage *a = new Usage;
        Usage *b = a->alloc();

        b->print();

        delete a;
        delete b;
    

    
        IAllocable *a = new Usage;
        Usage *b = dynamic_cast<Usage *>(a->alloc());

        b->print();

        delete a;
        delete b;
    
 

希望对你有帮助。

【讨论】:

【参考方案6】:

在C++中,有没有办法查询对象的类型...

是的,使用typeid() 运算符

例如:

// typeid, polymorphic class
 #include <iostream>
 #include <typeinfo>
 #include <exception>
 using namespace std;

 class CBase  virtual void f() ;
 class CDerived : public CBase ;

 int main () 
   try 
     CBase* a = new CBase;
     CBase* b = new CDerived;
      cout << "a is: " << typeid(a).name() << '\n';
     cout << "b is: " << typeid(b).name() << '\n';
     cout << "*a is: " << typeid(*a).name() << '\n';
     cout << "*b is: " << typeid(*b).name() << '\n';
     catch (exception& e)  cout << "Exception: " << e.what() << endl; 
    return 0;
  

输出

a is: class CBase *
b is: class CBase *
*a is: class CBase
*b is: class CDerived

如果 typeid 计算的类型是一个以取消引用运算符 (*) 开头的指针,并且该指针具有空值,则 typeid 会抛出 bad_typeid 异常

阅读more.....

【讨论】:

这不允许您创建该类型的新对象。 我可以使用 typeid 返回的值来创建该类型的新对象吗?我不想硬编码任何东西。 这只是允许知道该对象的确切类型。 另请注意name() 返回的名称是特定于实现的,可能不是非常有用(尽管您也可以使用特定于实现的解构函数)。 小心typeid().name(),因为它是特定于实现的。在 g++ 4.0.1 下,此示例打印:a is: P5CBase b is: P5CBase...【参考方案7】:
class Base

public:
 virtual ~Base()  
;

class Foo : public Base


;

class Bar : public Base


;

template<typename T1, typename T2>
T1* fun(T1* obj)

 T2* temp = new T2();
 return temp;


int main()

  Base* b = new Foo();
  fun<Base,Foo>(b);

【讨论】:

【参考方案8】:

当有非常多的类派生自同一个基类时,此代码将使您不必在每个类中都包含克隆方法。这是一种更方便的克隆方式,涉及模板和中间子类。如果层次足够浅,这是可行的。

struct PureBase 
    virtual Base* Clone() 
        return nullptr;
    ;
;

template<typename T>
struct Base : PureBase 
    virtual Base* Clone() 
        return new T();
    
;

struct Derived : Base<Derived> ;

int main() 
    PureBase* a = new Derived();
    PureBase* b = a->Clone(); // typeid(*b) == typeid(Derived)

【讨论】:

以上是关于从动态类型信息创建新对象的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中使用类型和属性名称从 XML 创建动态对象?

Java重要技术(27)动态代理之查看代理对象的类型信息

如何从QML基本类型列表中动态删除元素?

第六章 动态类型简介

Java SE基础2:Class类与反射

JavaScript之面向对象学习七(动态原型模式和寄生构造函数模式创建自定义类型)