Mehrere Datentypen in Vector



  • Hallo Community,

    ich brauche für eine Struktur eine Möglichkeit, um in einen Vector verschiedene Datentypen hinzuzufügen.

    Mein Gedanke war eine Art HolderClass die mit Templates arbeitet, aber irgendwie weis ich jetzt nicht genau wie ich diese dazu bringe, das man diese egal welchen Datentyp diese Repräsentiert, in den Vector einzufügen. Ging das nicht irgendwie mit Vererbung?

    Hier mein Code:

    template<class T>
        class DataHolder: public std::enable_shared_from_this<DataHolder<T>> {
        public:
            std::string key;
            T value;
    
            std::shared_ptr<DataHolder> prev;
            std::shared_ptr<DataHolder> next;
    
            std::shared_ptr<std::vector<DataHolder>> root;
    
        	std::shared_ptr<DataHolder<T>> getptr() { return this->shared_from_this(); }
        };
    

    Versucht habe ich es mit:

    std::vector<std::shared_ptr<DataHolder>> data;
    


  • Guck dir mal boost::any und/oder boost::variant<..> an - wenn du nicht Boost verwenden kannst, kannst du dennoch dasselbe Konzept anwenden.



  • Ich hatte mir schon eine Variante mit einen Any gebastelt, aber das bringt mir nix, ich brauch die Typeinfo wieder am Ende...



  • Schau dir das an:

    #include <iostream>
    #include <string>
    #include <typeinfo>
    #include <vector>
    using namespace std::literals::string_literals;
    
    // definition of the common interface of all holders
    // type returns the type of the contained value
    // data returns the address of the contained value
    // clone creates a new holder containing a copy of the current holder
    struct basic_holder
    {
    	virtual std::type_info const & type() const = 0;
    	virtual void const * data() const = 0;
    	virtual basic_holder * clone() const = 0;
    };
    
    // a concrete holder for some type
    // just basic implementations for the interface above
    template <typename Type>
    struct holder : basic_holder
    {
    	// needs a copyable Type
    	holder(Type const & value)
    		: value { value }
    	{
    	}
    
    	virtual std::type_info const & type() const override
    	{
    		// as easy as it gets
    		return typeid(Type);
    	}
    
    	virtual void const * data() const override
    	{
    		// pretty obvious
    		return static_cast<void const *>(&value);
    	}
    
    	virtual basic_holder * clone() const override
    	{
    		// yupp, also only works if Type is copyable
    		// but this is already a requirement of the holder<Type> constructor
    		return new holder<Type> { value };
    	}
    
    private:
    	// the stored value
    	Type value;
    };
    
    // this is the actual class used
    // holder and basic_holder are just implementation details
    // but any is what you want to use
    // it can be given any copyable value, is itself copyable and movable
    // and can return the contained value using the to<Type>() function
    class any
    {
    public:
    	// default constructor creates an empty any
    	any() : content { nullptr } {}
    
    	// create an any with a value
    	template <typename Type>
    	any(Type const & value)
    		: content { new holder<Type> { value } }
    	{
    	}
    
    	// cleanup
    	~any()
    	{
    		delete content;
    	}
    
    	// copyable
    	any(any const & orig)
    	{
    		content = orig.content ? orig.content->clone() : nullptr;
    	}
    	any & operator = (any const & orig)
    	{
    		delete content;
    		content = orig.content ? orig.content->clone() : nullptr;
    		return *this;
    	}
    
    	// movable
    	any(any && orig)
    	{
    		content = orig.content;
    		orig.content = nullptr;
    	}
    	any & operator = (any && orig)
    	{
    		delete content;
    		content = orig.content;
    		orig.content = nullptr;
    		return *this;
    	}
    
    	// assign a new value, the contained holder may change its type here
    	// basically the older holder is thrown away and a new one is created with the new type
    	template <typename Type>
    	any & operator = (Type const & value)
    	{
    		delete content;
    		content = new holder<Type> { value };
    		return *this;
    	}
    
    	// typesafe accessor to obtain the contained value
    	template <typename Type>
    	Type to() const
    	{
    		if (content && content->type() == typeid(Type))
    			return *static_cast<Type const *>(content->data());
    		throw std::bad_cast {};
    	}
    
    private:
    	basic_holder * content;
    };
    
    // a little usage example
    int main()
    {
    	// this specific any does not support C String literals, which is why std::string is used
    	std::vector<any> v { 23, 42, 1337, 3.1415926, "hello world"s, 9000 };
    
    	for (std::size_t n = 0u; n < v.size(); ++n)
    	{
    		try
    		{
    			auto const value = v[n].to<int>();
    			std::cout << "v[" << n << "] => " << value << "\n";
    		}
    		catch (std::bad_cast const &)
    		{
    			std::cout << "v[" << n << "] => not an int\n";
    		}
    	}
    	return 0;
    }
    

    Prinzipiell kannst du hier das any auch noch so erweitern, dass es dir die type()-Funktion vom basic_holder durchreicht.



  • Eine weniger aufwendige Variante könnte auch folgendes sein:

    #include <iostream>
    #include <string>
    #include <vector>
    using namespace std;
    
    enum class DataType
    {
        Int,
        Float,
        String,
    };
    
    class Data
    {
    public:
        Data(int iv)
            : stored_type { DataType::Int }
            , iv { iv }
        {
        }
    
        Data(float nv)
            : stored_type { DataType::Float }
            , nv { nv }
        {
        }
    
        Data(char const * pv)
        	: stored_type { DataType::String }
        	, pv { pv }
    	{
    	}
    
        std::string to_string() const
        {
        	switch (stored_type)
        	{
        		case DataType::Int: return std::to_string(iv);
        		case DataType::Float: return std::to_string(nv);
        		case DataType::String: return { pv ? pv : "" };
        	}
        	return {};
        }
    
    private:
        DataType stored_type;
        union {
            int iv;
            float nv;
            char const * pv;
        };
    };
    
    std::ostream & operator << (std::ostream & os, Data const & d)
    {
    	return os << d.to_string();
    }
    
    int main()
    {
    	std::vector<Data> foo { 23, 13.37f, "test" };
    	for (auto && d : foo)
    	{
    		std::cout << d << std::endl;
    	}
    	return 0;
    }
    

  • Mod

    any & operator = (any const & orig)
    	{
     		delete content;
    		content = orig.content ? orig.content->clone() : nullptr;
    		return *this;
    	 }
    
    	any & operator = (any && orig)
    	{
    		delete content;
    		content = orig.content;
    		orig.content = nullptr;
    		return *this;
    	}
    
    	// assign a new value, the contained holder may change its type here
    	// basically the older holder is thrown away and a new one is created with the new type
    	template <typename Type>
    	any & operator = (Type const & value)
    	{
    		delete content;
    		content = new holder<Type> { value };
    		return *this;
    	}
    

    Bei Meyers nichts gelernt? Das ist nicht mal im Ansatz (stark) exceptionsicher oder tolerant gegenüber Selbstzuweisung. Dabei nicht vergessen, dass hier mehr als bloss bad_alloc geworfen werden könnte.

    // typesafe accessor to obtain the contained value
    	template <typename Type>
    	Type to() const
    	{
    		if (content && content->type() == typeid(Type))
    			return *static_cast<Type const *>(content->data());
    		throw std::bad_cast {};
    	}
    

    Hier wäre es zweckmässig, nicht den Typ von Type zu prüfen, sondern den des Holders. Dann braucht man kein typeid, und kann auf umständliche Hacks über rohe Daten verzichten und es klappt auch mit qualifizierten Typen und Referenzen, also z.B.

    template <typename Type>
        Type to() const
        {
            if (auto p = dynamic_cast<const holder<Type>*>(content))
                return p->value;
            throw std::bad_cast {};
        }
    


  • camper schrieb:

    ...
    

    Bei Meyers nichts gelernt? Das ist nicht mal im Ansatz (stark) exceptionsicher oder tolerant gegenüber Selbstzuweisung. Dabei nicht vergessen, dass hier mehr als bloss bad_alloc geworfen werden könnte.

    Zugegeben, das war schnell zusammengefrickelt und ich habe einfach nicht drangedacht. Generell sollte aber auch nur das Konzept demonstriert werden.
    Code von Fremden aus dem Internet ohne Hinterfragen zu übernehmen ist ohnehin nicht ratsam.

    camper schrieb:

    // typesafe accessor to obtain the contained value
    	template <typename Type>
    	Type to() const
    	{
    		if (content && content->type() == typeid(Type))
    			return *static_cast<Type const *>(content->data());
    		throw std::bad_cast {};
    	}
    

    Hier wäre es zweckmässig, nicht den Typ von Type zu prüfen, sondern den des Holders. Dann braucht man kein typeid, und kann auf umständliche Hacks über rohe Daten verzichten und es klappt auch mit qualifizierten Typen und Referenzen, also z.B.

    template <typename Type>
        Type to() const
        {
            if (auto p = dynamic_cast<const holder<Type>*>(content))
                return p->value;
            throw std::bad_cast {};
        }
    

    Tatsächlich sieht das cooler aus. An das Verfahren habe ich bisher gar nicht gedacht.


Anmelden zum Antworten