C++将多种类型推入向量

16

注意: 我知道类似的问题在SO上已经被问过了,但我觉得它们不够有用或者不是很清晰。

第二点注意: 对于这个项目/任务的范围,我试图避免使用第三方库,比如Boost。

我想要看看是否有一种方法可以让一个单一的向量容纳多种类型,在每个索引中。例如,假设我有以下代码示例:

vector<something magical to hold various types> vec;
int x = 3;
string hi = "Hello World";
MyStruct s = {3, "Hi", 4.01};

vec.push_back(x);
vec.push_back(hi);
vec.push_back(s);

我听说 vector<void*> 可以工作,但是在内存分配方面会变得棘手,并且如果将一个值插入到特定索引中,而它的大小超出了预期,则总有可能意外覆盖附近内存中的某些部分。

在我的实际应用程序中,我知道可能会插入到向量中的类型,但这些类型并不都来自于同一个基类,并且不能保证所有这些类型都会被推送到向量中或以什么顺序推送。

有没有一种方法可以安全地完成我在代码示例中演示的目标?

感谢您的时间。


@chris 抱歉我表达不够清晰。对于这个项目/任务的范围,我正在尝试避免使用第三方库。我已经更新了我的问题。 - Oliver Spryn
3
如果您强制执行,就无法“了解”实际被推送的内容,我们是否可以假设这个向量除指针外没有拥有任何东西?如果在所有者级别上没有建立类型信息的方式,那么您在正确管理清除或抹除方面会遇到一些问题。具有指针和析构函数引用的结构体是可行的,但此时您必须开始问自己提供的真正数据模型是什么? - WhozCraig
你实际上需要完全随机的数据吗,还是它们之间有某种关系?如果存在某种类型层次结构,您可以存储指向它们所共享的更通用类型的指针,并使用shared_ptr使内存不那么混乱。 - Brendan Long
仔细阅读您的问题后,看起来您知道可以推动的领域是有限的,只是没有根据共同基础,对吗? - WhozCraig
尝试理解这里的推理(http://www.codeproject.com/Articles/23304/High-Performance-Heterogeneous-Container),然后你就可以得到一个异构容器。如果您需要真正的“任何”类型,请阅读“超出标准库”的内容,了解如何使用any,或者尝试不安全的方式 - 使用void* +管理。 - Dmitry Ledentsov
显示剩余11条评论
3个回答

22

std::vector<T>所持有的对象需要是同种类型。如果你需要将不同类型的对象放入一个向量中,你需要消除它们的类型并使它们看起来相似。你可以使用类似于boost::anyboost::variant<...>的道德等效物。 boost::any的想法是封装一个类型层次结构,存储指向基础的指针,但指向一个模板化的派生类。一个非常粗糙和不完整的轮廓如下:

#include <algorithm>
#include <iostream>

class any
{
private:
    struct base {
        virtual ~base() {}
        virtual base* clone() const = 0;
    };
    template <typename T>
    struct data: base {
        data(T const& value): value_(value) {}
        base* clone() const { return new data<T>(*this); }
        T value_;
    };
    base* ptr_;
public:
    template <typename T> any(T const& value): ptr_(new data<T>(value)) {}
    any(any const& other): ptr_(other.ptr_->clone()) {}
    any& operator= (any const& other) {
        any(other).swap(*this);
        return *this;
    }
    ~any() { delete this->ptr_; }
    void swap(any& other) { std::swap(this->ptr_, other.ptr_); }

    template <typename T>
    T& get() {
        return dynamic_cast<data<T>&>(*this->ptr_).value_;
    }
};

int main()
{
    any a0(17);
    any a1(3.14);
    try { a0.get<double>(); } catch (...) {}
    a0 = a1;
    std::cout << a0.get<double>() << "\n";
}

+1,好回答。但是请记住,在数据中,clone()应该是data <T> * clone() const,因为它是协变的 :) 但这可以说是比已接受的答案更好的答案。 - Moo-Juice
需要知道类型才能获取数据的方式,使得该解决方案对许多应用程序不起作用 - 即使用a0.get<double>()。如果我们假设我们知道类型,为什么不只使用void指针呢? - Matthaeus Gaius Caesar
1
@MatthaeusGaiusCaesar 差异可能很小,但您可以使用dynamic_cast>检查实际上有哪种类型。如果您有意愿,可以做得更好,例如,通过将类型检查调度到生成std::type_info const&virtual函数中。Boost anystd :: any(在C++17中添加)实际上就是这样做的,而将其添加到简短的演示实现中并不难。该代码只是基本草稿,并且答案也是如此。为了使它更好,您需要添加一些内容-或者使用std::any,因为当时回答时它不存在。 - Dietmar Kühl

9
为了实现这一点,你肯定需要一个包装类,以某种方式隐藏向量中对象的类型信息。
最好让这个类在你尝试获取 Type-A 时抛出异常,而你先前已将 Type-B 存储到其中。
以下是我项目中 Holder 类的一部分。你可以从这里开始。
注意:由于使用了不受限制的联合体,这仅适用于 C++11。有关更多信息,请参见此处:C++11 中提出的 Unrestricted Unions 是什么?
class Holder {
public:
    enum Type {
        BOOL,
        INT,
        STRING,
        // Other types you want to store into vector.
    };

    template<typename T>
    Holder (Type type, T val);

    ~Holder () {
        // You want to properly destroy
        // union members below that have non-trivial constructors
    }

    operator bool () const {
        if (type_ != BOOL) {
           throw SomeException();
        }
        return impl_.bool_;
    }
    // Do the same for other operators
    // Or maybe use templates?

private:
    union Impl {
        bool   bool_;
        int    int_;
        string string_;

        Impl() { new(&string_) string; }
    } impl_;

    Type type_;

    // Other stuff.
};

我非常确定你不能将std::string放入联合体中,因为根据9.5/1规定,如果它具有非平凡的构造函数、复制构造函数、析构函数或复制赋值运算符,则不能这样做。 - Mark B
2
你现在可以使用C++11了,它拥有所有的好处 :p 你可以参考wikipedia - Jimmy Lu
2
你应该在回答中注明这是一个仅适用于C++11的解决方案,并注意你必须使用放置new和显式销毁来安全地使用string成员。 - Mark B
修改了我的答案,指出它仅适用于C++11 :-) - Jimmy Lu
1
有人对这段代码进行了性能分析吗?这个解决方案看起来很干净,但是它比传统的多态+虚拟或dynamic_cast<>更快还是更慢? - Yohaï-Eliel Berreby

9

建议您可以使用各种形式的联合、变体等。根据存储对象的用途,外部多态性可能正是您想要的,如果您可以在基类接口中定义所有必要的操作

以下是一个示例,如果我们只想将对象打印到控制台:

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class any_type
{
public:
   virtual ~any_type() {}
   virtual void print() = 0;
};

template <class T>
class concrete_type : public any_type
{
public:
   concrete_type(const T& value) : value_(value)
   {}

   virtual void print()
   {
      std::cout << value_ << '\n';
   }
private:
   T value_;
};

int main()
{
   std::vector<std::unique_ptr<any_type>> v(2);

   v[0].reset(new concrete_type<int>(99));
   v[1].reset(new concrete_type<std::string>("Bottles of Beer"));

   for(size_t x = 0; x < 2; ++x)
   {
      v[x]->print();
   }

   return 0;
}

1
有没有一种方法,可以使用v[x].get()函数而不是print()函数,并且你可以以某种方式使用v[x] :: T result = v[x].get() - David Doria
建议在您的描述中加粗显示“如果您可以在基类接口中定义所有必要的操作”,这是至关重要的,否则解决方案将无法正常工作。值得注意的是,成员变量也是如此。 - chutsu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接