C++ std::map 可以保存任意类型的值

54
基本上我想要一个名为MyClass的类,它拥有一个哈希映射,将字段名称(字符串)映射到任何类型的值。为此,我编写了一个单独的MyField类,用于保存类型和值信息。
目前为止,这是我的实现:
template <typename T>
class MyField {
    T m_Value;
    int m_Size;
}


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

但是你可以看到,地图声明失败是因为我没有为MyField提供类型参数... 所以我猜应该是这样的:
std::map< string, MyField<int> > fields;

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


但显然这会破坏我的整个目的,因为声明的map只能持有特定类型的MyField..我想要一个可以持有任何类型的MyField类的map..

有没有办法实现这个需求呢?


5
你需要一种类型擦除的方法。我建议使用boost::any - chris
你可以使用 std::map<std::string, std::shared_ptr<void>> - Bill Lynch
@sharth你使用shared_ptr<void>的原因是什么?而不是简单地使用(void*)? - user3794186
1
我认为这样使用类型擦除就有些过了,使用 void* (shared_ptr<void> 是相同的)。你需要至少一个额外值来确定那个东西是什么。除非你在编写真正低级别的代码,否则我会使用指向基类的指针或 boost::variant<>(如果不可用)。 - Blindy
如果真的只是任何 MyField<T>,我想另一个选项是一个基类,每个 MyField<T> 都继承自该基类。 - chris
@user3794186:为了实现你的目标,这两种方法没有太大区别。但是其中一种方法可以使内存管理更加简单。 - Bill Lynch
8个回答

72

这在 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
}

2
进一步简化,有没有办法用 auto year2 = notebook["Born"]); 替换 int year2 = std::any_cast<int>(notebook["Born"]); - solvingPuzzles

41

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)创建对象。这意味着实际对象是在自由存储区(堆)中创建的,并且可以由分配器(通常是默认分配器)提供的任何位置。因此,由于缓存未命中,经常遍历对象列表并不像可能那么快。

diagram of vector of polymorphic objects

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

diagram of polymorphic container

这个想法是容器会管理同种类型的对象数组,但仍然保持相同的接口。这个接口可以是一个概念(使用鸭子类型技术)或动态接口(像我第一个例子中的基类)。

优点是容器会将同种类型的对象保存在单独的向量中,因此遍历它们很快。只有从一种类型到另一种类型的转换不快。

以下是一个示例(图片来自此处):http://bannalia.blogspot.fr/2014/05/fast-polymorphic-collections.html

然而,如果您需要保持插入对象的顺序,则此技术失去了其吸引力。

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


作为对这个答案的补充,我想指出一些非常好的博客文章,总结了所有你可以使用的C++类型抹除技术,附有评论和优缺点:

20

使用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),安全和不安全(愚蠢的转换)值检索,具有const正确性,RHS操作数和指针和值类型。

对于您构建整个应用程序的低级基本类型,这一点通常是正确的,甚至更加重要。


14
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;

我一定会尝试这个!你能给我解释一下AnyBase有两个析构函数吗?我不太明白为什么你需要两个以及它们的作用..谢谢。 - user3794186
@user3794186,这只是实现问题,C++中纯虚析构函数必须有一个实现。我认为重点是使基类成为抽象类(因此无法创建),更清晰的方法是使用私有构造函数。 - Blindy
为什么 AnyBase 析构函数的定义需要是内联的? - Pedro Batista
@PedroBatista 不一定非得这样,但如果不是这样的话,它的定义就必须在 cpp 文件中而不是头文件中。因此,为了简单起见,我将其设置为内联函数作为示例。 - Neil Kirk
@Blindy 无论你如何定义构造函数,它都需要有一个虚析构函数。 - Neil Kirk
显示剩余4条评论

5

C++17有一个名为std::variant的类型,比union更好地实现了持有不同类型的功能。

对于没有使用C++17的人,boost::variant实现了相同的机制。

对于没有使用boost的人,https://github.com/mapbox/variant实现了一个更轻量级的variant版本,适用于C++11和C++14,看起来非常有前途,文档齐全,轻巧,并且有大量使用示例。


2

您也可以使用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;
}

2
s/reinterpret_cast/static_cast/ - chris

1

这是一种天真的做法。当然,你可以添加包装器来避免一些样板代码。

#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;
}

-3

正在进行中。这种方法的优点是在进行赋值时不需要转换任何内容,或者下面列出的任何功能。

目前它可以:

  • 存储非容器字面类型(const char*、double、int、float、char、bool)
  • 使用ostream运算符输出相应键的值
  • 重新分配现有键的值
  • 仅使用append方法添加新的键值对,键不能相同,否则会得到错误消息
  • 使用+运算符添加相同类型的字面量

在代码中,我已经在主函数中演示了它目前能做什么。

/*
This program demonstrates a map of arbitrary literal types implemented in C++17, using any. 
*/

#include <vector>
#include <any>
#include <utility>
#include <iostream>
using namespace std;

class ArbMap
{
    public:
    ArbMap() : vec({}), None("None") {} //default constructor
    
    ArbMap(const vector < pair<any,any> > &x) //parametrized constructor, takes in a vector of pairs
    : vec(x), None("None") {}

    //our conversion function, this time we pass in a reference
    //to a string, which will get updated depending on which 
    //cast was successful. Trying to return values is ill-advised
    //because this function is recursive, so passing a reference
    //was the next logical solution
    void elem(any &x, string &temp, int num=0 )
    {
        try
        {
            switch (num)
            {
                case 0:
                any_cast<int>(x);
                temp = "i";
                break;
                case 1:
                any_cast<double>(x);
                temp = "d";
                break;
                case 2:
                any_cast<const char*>(x);
                temp = "cc";
                break;
                case 3:
                any_cast<char>(x);
                temp = "c";
                break;
                case 4:
                any_cast<bool>(x);
                temp = "b";
                break;
                case 5:
                any_cast<string>(x);
                temp = "s";
                break;
            }
        } 
        catch(const bad_cast& e)
        {
            elem(x,temp,++num);
        }
        
    }
    //returns size of vector of pairs 
    size_t size()
    {
        return vec.size();
    }


    /* Uses linear search to find key, then tries to cast 
    all the elements into the appropriate type. */
    any& operator[](any key)
    {
        ArbMap temp;        
        string stemp;
        for (size_t i = 0; i<vec.size(); ++i)
        {
            temp.elem(vec[i].first,stemp);
            if (stemp=="i")
            {
                try
                {
                    any_cast<int>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<int>(key)==any_cast<int>(vec[i].first))
                {
                    return vec[i].second;
                }
            } 
            else if (stemp=="d")
            {
                try
                {
                    any_cast<double>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<double>(key)==any_cast<double>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="cc")
            {
                try
                {
                    any_cast<const char*>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<const char*>(key)==any_cast<const char*>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="c")
            {
                try
                {
                    any_cast<char>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<char>(key)==any_cast<char>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="b")
            {
                try
                {
                    any_cast<bool>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<bool>(key)==any_cast<bool>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
        }
        //vec.push_back({key,None});
        throw -1;
        //return None;

    }
    void print();
    void append(any key, any value);
    private:
    vector < pair<any,any> > vec;
    any None;
};

ostream& operator<<(ostream& out, any a)
{
    ArbMap temp;        //should be updated to be a smart pointer?
    string stemp;
    temp.elem(a,stemp); //stemp will get updated in the elem function
    
    //The "if else-if ladder" for casting types
    if (stemp=="i") out << any_cast<int>(a); 
    else if (stemp=="d") out << any_cast<double>(a);
    else if (stemp=="cc") out << any_cast<const char*>(a);
    else if (stemp=="c") out << any_cast<char>(a);
    else if (stemp=="b") 
    {
        if (any_cast<bool>(a)==1)
        out << "true";
        else
        out << "false";
    }
    else if (stemp=="s") out << any_cast<string>(a);

    return out;
}

any operator+(any val1, any val2)
{
    ArbMap temp;        
    string stemp1, stemp2;
    temp.elem(val1,stemp1); 
    temp.elem(val2,stemp2); 

    try
    {
        if (stemp1 != stemp2)
            throw -1;

        if (stemp1 == "i")
        {
            return any_cast<int>(val1)+any_cast<int>(val2);
        }
        else if (stemp1 == "d")
        {
            return any_cast<double>(val1)+any_cast<double>(val2);
        }
        else if (stemp1 == "cc")
        {
            return string(any_cast<const char*>(val1))+string(any_cast<const char*>(val2));
        }
        else if (stemp1 == "c")
        {
            return string{any_cast<char>(val1)}+string{any_cast<char>(val2)};
        }
        else if (stemp1=="b")
        {
            return static_cast<bool>(any_cast<bool>(val1)+any_cast<bool>(val2));
        }


    }
    catch (int err)
    {
        cout << "Bad cast! Operands must be of the same 'type'.\n";
       
    }
    
    return val1;
    
}

void ArbMap::print()
{
    cout << '\n';
    for (size_t i = 0; i<vec.size(); ++i)
    {
        cout << vec[i].first << ": " << vec[i].second << '\n';
    }
    cout << '\n';
}

void ArbMap::append(any key, any value)
{
    try 
    {
        (*this)[key];
        throw "Already exists!";
        
    }
    catch(int error)
    {
        vec.push_back({key,value});
    }
    catch(const char* error)
    {
        cout << "ArbMap::append failed, key already exists!\n";
    }
    
}



int main() {
    ArbMap s({{1,2},{"aaa",1.2},{'c',33.3},{"what","this is awesome"}, {true, false}});
    
    cout << s[1] << '\n' << s["aaa"] << '\n' << s['c']
    << '\n' << s["what"] << '\n' 
    //Uncomment the line below and runtime error will occur, as 
    //entry is not in the dictionary
    // << s["not in the dictionary bro"] << '\n'
    << s[true] << '\n';
    
    
    s.print();
    s[1] = "hello";
    s.print();
    s.append(2.3,"what");
    s.print();
    s[2.3] = "hello";
    s.print();
    s.append(2.3,"what");
    s.print();
    s[1] = 1.2;
    s.print();
    s.append(2.4,1.2);
    
    //Operator +
    cout << s[1]+s[2.4] << '\n';
    cout << s["what"] + s[2.3] << '\n';
    s.append('d','a');
    cout << s['c'] << '\n';
    cout <<  s[2.4]+ s["aaa"]+ s['c'] + s['c'] + s['c'] << '\n';
    cout << s[true]+s[true] << '\n';

    return 0;
}

"Work in progress..." 的意思是什么? - YesThatIsMyName
我不知道你是否不喜欢它,但我认为我的解决方案是唯一一个允许使用任意类型的映射而无需进行任何强制转换的。它还在不断改进中,因为它不能像Python字典那样做所有事情,但我认为这是一个扎实的努力,绝对不值得被不喜欢。 - Edward Finkelstein
请编辑您的答案,以便您在此处作为评论写下的内容也写在您的答案中。 - YesThatIsMyName
1
好的,我添加了一些澄清。 - Edward Finkelstein
这看起来有点奇怪。如果您只支持有限数量的类型,那么应该使用std::variant。另外:为什么“using namespace std;”被认为是不好的实践? - HolyBlackCat
多种类型还没有被实现。是的,它有点奇怪,但C++不是Python,所以我不确定你们期望什么。如果你有一个在C++中实现相同效果的更好的解决方案,我很乐意看到它。 - Edward Finkelstein

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