C++ std::map 持有任何类型的值

Posted

技术标签:

【中文标题】C++ std::map 持有任何类型的值【英文标题】:C++ std::map holding ANY type of value 【发布时间】:2014-09-02 08:16:46 【问题描述】:

基本上,我希望 MyClass 包含一个 Hashmap,它将字段名称(字符串)映射到任何类型的 值.. 为此,我编写了一个单独的 MyField 类来保存类型和值信息..

这是我目前所拥有的:

template <typename T>
class MyField 
    T m_Value;
    int m_Size;



struct MyClass 
    std::map<string, MyField> fields;   //ERROR!!!

但是如你所见,map 声明失败是因为我没有为 MyField 提供类型参数...

所以我猜它必须是这样的

std::map< string, MyField<int> > fields;

std::map< string, MyField<double> > fields;

但这显然破坏了我的整个目的,因为声明的地图只能包含特定类型的 MyField。我想要一个可以包含任何类型的 MyField 类的地图。

有什么方法可以实现这个..?

【问题讨论】:

您需要某种类型的擦除。我推荐boost::any 你可以使用std::map&lt;std::string, std::shared_ptr&lt;void&gt;&gt; @sharth 你有什么理由使用 shared_ptr 而不是简单的 (void *)? 我认为使用void *shared_ptr&lt;void&gt; 是一样的)将类型擦除太过分了。你至少需要一个额外的价值来弄清楚那是什么。我要么使用指向基类的指针,要么使用 boost::variant&lt;&gt; 如果它不可用,除非你正在编写非常低级的代码。 如果它真的只是任何MyField&lt;T&gt;,我想另一种选择是每个MyField&lt;T&gt; 继承自的基类。 【参考方案1】:

使用boost::variant(如果您知道可以存储的类型,它会提供编译时支持)或boost::any(实际上适用于任何类型——但不太可能如此)。

http://www.boost.org/doc/libs/1_55_0/doc/html/variant/misc.html#variant.versus-any

编辑:我怎么强调都不为过,尽管推出自己的解决方案可能看起来很酷,但从长远来看,使用完整、正确的实现将为您省去很多麻烦。 boost::any 实现 RHS 复制构造函数 (C++11),包括安全 (typeid()) 和不安全(哑转换)值检索,具有 const 正确性、RHS 操作数以及指针和值类型。

这通常是正确的,但对于构​​建整个应用程序的低级基本类型更是如此。

【讨论】:

【参考方案2】:
class AnyBase

public:
    virtual ~AnyBase() = 0;
;
inline AnyBase::~AnyBase() 

template<class T>
class Any : public AnyBase

public:
    typedef T Type;
    explicit Any(const Type& data) : data(data) 
    Any() 
    Type data;
;

std::map<std::string, std::unique_ptr<AnyBase>> anymap;
anymap["number"].reset(new Any<int>(5));
anymap["text"].reset(new Any<std::string>("5"));

// throws std::bad_cast if not really Any<int>
int value = dynamic_cast<Any<int>&>(*anymap["number"]).data;

【讨论】:

您可以在基类中添加一个成员来获取值并避免额外的强制转换((Any&lt;int&gt; &amp;)(*anymap["number"]).data 看起来很糟糕)。但是此时您实现了一个基本的boost::any,还不如使用完整且经过良好测试的类。 +1 不过,创作很到位! 我一定会试试这个!你能向我解释一下 AnyBase 的两个析构函数吗?我不太明白为什么你有两个以及为什么需要它们......谢谢 @user3794186,只有一个析构函数,它是 virtual,因为否则派生类型的析构函数不会被调用(因此 data 析构函数不会被调用)。 @Blindy 哦,我明白了。那么内联析构函数呢..? @user3794186,这只是实现,纯虚析构函数必须在 C++ 中实现。我认为关键是使基类抽象(因此不可创建),更清晰的方法是使用私有构造函数。【参考方案3】:

Blindy 的回答非常好(+1),但只是为了完成回答:还有另一种方法可以在没有库的情况下使用动态继承:

class MyFieldInterface

    int m_Size; // of course use appropriate access level in the real code...
    ~MyFieldInterface() = default;


template <typename T>
class MyField : public MyFieldInterface 
    T m_Value; 



struct MyClass 
    std::map<string, MyFieldInterface* > fields;  

优点:

任何 C++ 编码人员都熟悉它 它不会强迫您使用 Boost(在某些情况下不允许使用);

缺点:

您必须在堆/空闲存储上分配对象并使用引用语义而不是值语义来操作它们; 以这种方式公开的公共继承可能会导致过度使用动态继承以及许多与您的类型相关的长期问题确实过于相互依赖; 如果指针向量必须拥有对象,则它会出现问题,因为您必须管理销毁;

因此,如果可以,请使用 boost::any 或 boost::variant 作为默认值,否则仅考虑此选项。

要解决最后一个缺点,您可以使用智能指针:

struct MyClass 
    std::map<string, std::unique_ptr<MyFieldInterface> > fields;  // or shared_ptr<> if you are sharing ownership

然而,还有一个潜在的更成问题的地方:

它强制您使用 new/delete(或 make_unique/shared)创建对象。这意味着实际对象是在分配器提供的任何位置(大多数是默认位置)的空闲存储(堆)中创建的。因此,由于cache misses,经常浏览对象列表并没有那么快。

如果您关心尽可能快地循环遍历此列表的性能(如果没有,请忽略以下内容),那么您最好使用 boost::variant(如果您已经知道您将使用的所有具体类型)或使用某种类型擦除的多态容器。

这个想法是容器将管理相同类型的对象数组,但仍然公开相同的接口。该接口可以是概念(使用鸭子类型技术)或动态接口(如我的第一个示例中的基类)。 优点是容器会将相同类型的对象保存在单独的向量中,因此通过它们很快。只有从一种类型转换到另一种类型不是。

这是一个例子(图片来自那里):http://bannalia.blogspot.fr/2014/05/fast-polymorphic-collections.html

但是,如果您需要保持插入对象的顺序,这种技术就会失去兴趣。

无论如何,有几种可能的解决方案,这在很大程度上取决于您的需求。如果您对自己的案例没有足够的经验,我建议使用我在示例中首先解释的简单解决方案或 boost::any/variant。


作为对这个答案的补充,我想指出非常好的博客文章,其中总结了您可以使用的所有 C++ 类型擦除技术,以及 cmets 和优缺点:

http://talesofcpp.fusionfenix.com/post-16/episode-nine-erasing-the-concrete http://akrzemi1.wordpress.com/2013/11/18/type-erasure-part-i/ http://akrzemi1.wordpress.com/2013/12/06/type-erasure-part-ii/ http://akrzemi1.wordpress.com/2013/12/11/type-erasure-part-iii/ http://akrzemi1.wordpress.com/2014/01/13/type-erasure-part-iv/

【讨论】:

【参考方案4】:

您还可以使用 void* 并使用 reinterpret_cast 将值转换回正确的类型。它是 C 中回调中常用的一种技术。

#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdint> // Needed for intptr_t
using namespace std;


enum TypeID 
    TYPE_INT,
    TYPE_CHAR_PTR,
    TYPE_MYFIELD
;    

struct MyField 
    int typeId;
    void * data;
;

int main() 

    std::unordered_map<std::string, MyField> map;

    MyField anInt = TYPE_INT, reinterpret_cast<void*>(42) ;

    char cstr[] = "Jolly good";
    MyField aCString =  TYPE_CHAR_PTR, cstr ;

    MyField aStruct  =  TYPE_MYFIELD, &anInt ;

    map.emplace( "Int", anInt );
    map.emplace( "C String", aCString );
    map.emplace( "MyField" , aStruct  );  

    int         intval   = static_cast<int>(reinterpret_cast<intptr_t>(map["Int"].data)); 
    const char *cstr2    = reinterpret_cast<const char *>( map["C String"].data );
    MyField*    myStruct = reinterpret_cast<MyField*>( map["MyField"].data );

    cout << intval << '\n'
         << cstr << '\n'
         << myStruct->typeId << ": " << static_cast<int>(reinterpret_cast<intptr_t>(myStruct->data)) << endl;

【讨论】:

s/reinterpret_cast/static_cast/【参考方案5】:

C++17 有一个 std::variant 类型,该类型具有比联合更好地保存不同类型的功能。

对于那些不在 C++17 上的人,boost::variant 实现了同样的机制。

对于那些不使用 boost 的人,https://github.com/mapbox/variant 为 C++11 和 C++14 实现了一个更轻量级的 variant 版本,看起来很有前途、有据可查、轻量级,并且有大量的使用示例。

【讨论】:

【参考方案6】:

这在 C++ 17 中很简单。使用 std::map + std::any + std::any_cast:

#include <map>
#include <string>
#include <any>
        
int main()

    std::map<std::string, std::any> notebook;

    std::string name "Pluto" ;
    int year = 2015;

    notebook["PetName"] = name;
    notebook["Born"] = year;

    std::string name2 = std::any_cast<std::string>(notebook["PetName"]); // = "Pluto"
    int year2 = std::any_cast<int>(notebook["Born"]); // = 2015

【讨论】:

【参考方案7】:

这是一种幼稚的做法。当然,您可以添加包装器以使某些样板代码无效。

#include <iostream>
#include <memory>
#include <map>
#include <vector>
#include <cassert>


struct IObject

    virtual ~IObject() = default;
;

template<class T>
class Object final : public IObject

public:
    Object(T t_content) : m_context(t_content)
    ~Object() = default;

    const T& get() const
    
        return m_context;
    

private:
    T m_context;
;

struct MyClass

    std::map<std::string, std::unique_ptr<IObject>> m_fields;
;


int main()


    MyClass yourClass;

    // Content as scalar
    yourClass.m_fields["scalar"] = std::make_unique<Object<int>>(35);
    
    // Content as vector
    std::vector<double> v 3.1, 0.042 ;
    yourClass.m_fields["vector"] = std::make_unique<Object<std::vector<double>>>(v);
       
    auto scalar = dynamic_cast<Object<int>*>(yourClass.m_fields["scalar"].get())->get();
    assert(scalar == 35);

    auto vector_ = dynamic_cast<Object<std::vector<double>>*>(yourClass.m_fields["vector"].get())->get();
    assert(v == vector_);

    return 0;

【讨论】:

以上是关于C++ std::map 持有任何类型的值的主要内容,如果未能解决你的问题,请参考以下文章

在普通键的情况下使用map over unordered_map有什么好处吗?

C++ : auto关键字

C++ 将预先保留的哈希映射(std::unordered_map)与整数键和连续数据数组(std::vector)进行比较

使用模板将类型发送到函数中

C++ std::map

std::unordered_map::find 使用不同于 Key 类型的类型?