40

基本上我希望 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 类的地图。

有什么办法可以做到这一点..?

4

7 回答 7

52

这在 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
}
于 2018-06-20T19:55:39.247 回答
38

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)创建对象。这意味着实际对象是在分配器提供的任何位置(大多数是默认位置)的空闲存储(堆)中创建的。因此,由于缓存未命中,经常浏览对象列表并没有可能那么快。

多态对象向量图

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

多态容器示意图

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

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

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

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


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

于 2014-07-11T16:39:44.670 回答
19

使用任何一种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 操作数以及指针和值类型。

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

于 2014-07-11T16:21:49.680 回答
12
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;
于 2014-07-11T16:27:26.587 回答
3

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

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

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

于 2017-10-16T17:48:14.707 回答
1

您还可以使用 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;
}
于 2014-07-11T16:46:35.987 回答
0

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

#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;
}
于 2021-10-30T11:10:50.180 回答