如何在一个列表中存储不同的数据类型? (C ++)

时间:2010-08-24 17:50:50

标签: c++ data-structures

我需要存储一个对象的各种属性的列表。属性由名称和数据组成,可以是任何数据类型。

我知道我可以创建一个类“Property”,并使用不同的PropertySubClasses扩展它,它们只与它们存储的数据类型不同,但感觉不对。

class Property
{
     Property(std::string name);
     virtual ~Property();

     std::string m_name;
};

class PropertyBoolean : Property
{
     PropertyBoolean(std::string name, bool data);

     bool m_data;
};

class PropertyFloat : Property
{
     PropertyFloat(std::string name, float data);

     float m_data;
};

class PropertyVector : Property
{
     PropertyVector(std::string name, std::vector<float> data);

     std::vector<float> m_data;
};

现在我可以在

中存储各种属性
 std::vector<Property*>

为了获取数据,我可以将对象强制转换为子类。或者我可以创建一个纯虚函数来对函数内部的数据执行某些操作,而无需进行转换。

无论如何,创建这些不同类型的子类是不对的,这些子类只是因为它们存储的数据类型而不同。有没有其他方便的方法来实现类似的行为?

我无法访问Boost。

7 个答案:

答案 0 :(得分:26)

C ++是一种多范式语言。它最明亮,在范式混合的情况下是最强大的。

class Property
{
public:
    Property(const std::string& name) //note: we don't lightly copy strings in C++
      : m_name(name) {}
    virtual ~Property() {}
private:
    std::string m_name;
};

template< typename T >
class TypedProperty : public Property
{
public:
    TypedProperty (const std::string& name, const T& data)
      : Property(name), m_data(data);
private:
    T m_data;
};

typedef std::vector< std::shared_ptr<Property> > property_list_type;

修改: 为什么使用std::shared_ptr<Property>代替Property*
请考虑以下代码:

void f()
{
  std::vector<Property*> my_property_list;
  for(unsigned int u=0; u<10; ++u)
    my_property_list.push_back(new Property(u));

  use_property_list(my_property_list);

  for(std::vector<Property*>::iterator it=my_property_list.begin();
                                      it!=my_property_list.end(); ++it)
    delete *it;
}

那个for循环试图清除,删除向量中的所有属性,就在它超出范围之前并带走所有指针。
现在,虽然这对初学者来说似乎没什么问题,但如果你是一个经验丰富的C ++开发人员,那么一旦你看到它,代码应该会引起警钟。

问题是对use_property_list()的调用可能会引发异常。如果是这样,函数f()将立即被删除。为了正确清理,将调用f()中创建的所有自动对象的析构函数。也就是说,my_property_list将被正确销毁。然后std::vector的析构函数将很好地清理它所拥有的数据。 但是,它包含指针 std::vector应该如何知道这些指针是否是引用其对象的最后一个指针?
由于它不知道,它不会删除对象,它只会在销毁内容时销毁指针,让你在堆上找不到任何指针的对象。这就是所谓的“泄漏”。

为了避免这种情况,您需要捕获所有异常,清理属性以及重新抛出异常。但是,从现在起十年后,有人必须为10MLoC应用程序添加一个新功能,这个功能已经发展到了,并且匆忙地添加了代码,当某些条件成立时,代码会过早地保留该功能。代码经过测试,它可以运行并且不会崩溃 - 只有它现在所属的服务器每小时泄漏几个字节,因为每周一次内存不足而导致崩溃。查找可以进行数小时的精细调试。

底线:永远不要手动管理资源,始终将它们包装在专门处理此类资源的一个实例的类的对象中。对于动态分配的对象,这些句柄称为“智能指针”,最常用的句柄是shared_ptr

答案 1 :(得分:9)

较低级别的方法是使用联合

class Property
  union {
    int int_data;
    bool bool_data;
    std::cstring* string_data;
  };
  enum { INT_PROP, BOOL_PROP, STRING_PROP } data_type;
  // ... more smarts ...
};

Dunno为什么你的其他解决方案感觉不对,所以我不知道这种方式对你来说是不是更好。

编辑:提供一些使用示例的代码。

Property car = collection_of_properties.head();
if (car.data_type == Property::INT_PROP) {
  printf("The integer property is %d\n", car.int_data);
} // etc.

我可能会在可能的情况下将这种逻辑放入类的方法中。您还可以使用此构造函数之类的成员来保持数据和类型字段同步:

Property::Property(bool value) {
  bool_data = value;
  data_type = BOOL_PROP;
}

答案 2 :(得分:4)

我建议boost::variantboost::any。 [Related question]

答案 3 :(得分:2)

编写一个模板类Property<T>,该类来自Property,其数据成员类型为T

答案 4 :(得分:1)

另一种可能的解决方案是编写一个管理指向Property类的指针的中间类:

class Bla {
private:
  Property* mp
public:
  explicit Bla(Property* p) : mp(p) { }

  ~Bla() { delete p; }

  // The standard copy constructor
  // and assignment operator
  // aren't sufficient in this case:
  // They would only copy the 
  // pointer mp (shallow copy)
  Bla(const Bla* b) : mp(b.mp->clone()) { }

  Bla& operator = (Bla b) { // copy'n'swap trick
    swap(b);
    return *this;
  }

  void swap(Bla& b) {
    using std::swap; // #include <algorithm>
    swap(mp, b.mp);
  }

  Property* operator -> () const {
    return mp;
  }

  Property& operator * () const {
    return *mp;
  }
};

您必须向类添加一个虚拟clone方法,返回指向新创建的自身副本的指针:

class StringProperty : public Property {
// ...
public:
  // ...
  virtual Property* clone() { return new StringProperty(*this); }
  // ...
};

然后你就可以做到这一点:

std::vector<Bla> v;
v.push_back(Bla(new StringProperty("Name", "Jon Doe")));
// ...
std::vector<Bla>::const_iterator i = v.begin();
(*i)->some_virtual_method();

保留v的范围意味着将销毁所有Bla s自动释放他们持有的指针。由于其重载引用和间接运算符,类Bla的行为类似于普通指针。在最后一行*i返回对Bla对象的引用,使用->表示与指向Property对象的指针相同。

这种方法的一个可能的缺点是,如果必须复制中间对象,则始终获得堆操作(newdelete)。例如,如果超出向量的容量,则必须将所有中间对象复制到新的内存中。

在新标准(即c ++ 0x)中,您将能够使用unique_ptr模板:它

  • 可以在标准容器内使用(与auto_ptr不同,不能在标准容器中使用),
  • 提供通常更快的移动语义(它可以轻松传递)和
  • 注意保留的指针(它会自动释放它们)。

答案 5 :(得分:1)

我看到现在有很多尝试解决你的问题,但我觉得你看错了 - 为什么你实际想要这样做首先?基类中是否有一些有趣的功能,你省略了指定?

你被迫打开一个属性类型id来做你想要的特定实例的事实是代码气味,尤其是当子类完全没有任何共同点时通过除了名称之外的基类(在这种情况下是类型id)。

答案 6 :(得分:0)

您可以使用Boost库执行此操作,或者您可以创建一个类型代码和指向数据的void指针的类,但这意味着放弃一些C ++的类型安全性。换句话说,如果你有一个属性“foo”,其值是一个整数,并给它一个字符串值,编译器将不会为你找到错误。

我建议您重新审视您的设计,并重新评估您是否真的需要这么大的灵活性。你真的需要能够处理任何类型的属性吗?如果您可以将其缩小到几种类型,您可以使用继承或模板来提出解决方案,而无需“对抗语言”。