简体   繁体   中英

A map in c++ which can accept any type of value

I want to create a map in c++ which can accept any type of value, i did the same in java using Object class Map but not getting how to do it in c++. Please help.

As the previous answer correctly suggested, you can't do it out of the box in C++. I am assuming that by "[...] which can accept any type of value [...]" you mean the value, not the key of the map.

Here is what you can do, though. You have two options; I'll go from ugly to nice.

First approach:

  • Create a value holding class which you will be using as a value for the map. Let's call it Value .

  • Implement explicit constructors in that class for all types you want to support, and keep track on the type of value the class is currently storing

  • Check the value's type after getting it from the map and use the appropriate getter function

  • Optionally, overload the << operator to support standard streams

For a sample implementation, see the following code:

#include <iostream>
#include <memory>
#include <map>

class Value {
public:
  typedef enum {
    String,
    Integer,
    Double,
    Float
  } ContentType;

private:
  ContentType m_ctType;

  std::string m_strContent;
  int m_nContent;
  double m_dContent;
  float m_fContent;

public:
  Value() : m_strContent(""), m_ctType(String) {}
  explicit Value(const char* arrcString) : m_strContent(std::string(arrcString)), m_ctType(String) {}
  explicit Value(std::string strContent) : m_strContent(strContent), m_ctType(String) {}
  explicit Value(int nContent) : m_nContent(nContent), m_ctType(Integer) {}
  explicit Value(double dContent) : m_dContent(dContent), m_ctType(Double) {}
  explicit Value(float fContent) : m_fContent(fContent), m_ctType(Float) {}

  ~Value() {}

  ContentType type() {
    return m_ctType;
  }

  std::string stringValue() { return m_strContent; }
  int integerValue() { return m_nContent; }
  double doubleValue() { return m_dContent; }
  float floatValue() { return m_fContent; }
};

std::ostream& operator<<(std::ostream& osStream, Value& valOut) {
  switch(valOut.type()) {
  case Value::String: osStream << valOut.stringValue(); break;
  case Value::Integer: osStream << valOut.integerValue(); break;
  case Value::Double: osStream << valOut.doubleValue(); break;
  case Value::Float: osStream << valOut.floatValue(); break;
  }

  return osStream;
}

This is usable like so:

int main() {
  std::map<int, Value> mapAnyValue;

  mapAnyValue[0] = Value("Test");
  mapAnyValue[1] = Value(1337);

  std::cout << mapAnyValue[0] << ", " << mapAnyValue[1] << std::endl;

  return 0;
}

This outputs

Test, 1337

Now some might argue that this is

  • Inefficient (it reserved fields for types that are not used in every Value instance)
  • Difficult to extend/maintain (adding new fields is kind of cumbersome)
  • In general bad design

and they are right. So here is an alternative using polymorphism and templates.

Second approach:

This requires you to define the type of value you want to store when assigning it to a variable, and it requires the use of pointers. The reasons are given below.

For this approach, we do the following:

  • Create a base class ValueBase that serves as a class we can put in our map as value type.

  • Derive from this class a templated class Value<T> that holds an arbitrary value of template type T .

  • To support std::cout and friends, we implement an operator overloading for << for class ValueBase , add a pure virtual output function to ValueBase , and override this function in Value<T> to make use of the default << operator for any type you are using in the template.

See below for a code sample:

#include <iostream>
#include <memory>
#include <map>

class ValueBase {
public:
  ValueBase() {}
  ~ValueBase() {}

  virtual void output(std::ostream& osStream) = 0;
};

template<typename T>
class Value : public ValueBase {
private:
  T m_tValue;

public:
  Value(T tValue) : m_tValue(tValue) {}
  ~Value() {}

  T value() {
    return m_tValue;
  }

  void output(std::ostream& osStream) override {
    osStream << m_tValue;
  }
};

std::ostream& operator<<(std::ostream& osStream, ValueBase& valOut) {
  valOut.output(osStream);

  return osStream;
}

This is usable like so:

int main() {
  std::map<int, std::shared_ptr<ValueBase>> mapAnyValue;

  mapAnyValue[0] = std::make_shared<Value<std::string>>("Test");
  mapAnyValue[1] = std::make_shared<Value<int>>(1337);

  std::cout << *mapAnyValue[0] << ", " << *mapAnyValue[1] << std::endl;

  return 0;
}

Or without smart pointers:

int main() {
  std::map<int, ValueBase*> mapAnyValue;

  mapAnyValue[0] = new Value<std::string>("Test");
  mapAnyValue[1] = new Value<int>(1337);

  std::cout << *mapAnyValue[0] << ", " << *mapAnyValue[1] << std::endl;

  delete mapAnyValue[0];
  delete mapAnyValue[1];

  return 0;
}

Both output

Test, 1337

There are a couple of differences in terms of usage for the second approach.

First of all, you need to use pointers. The reason for this is that this way, the member function vtable is preserved and you can override functions in the base class from derived classes. In our situation, this means: When we call output() on a pointer of type ValueBase that was initialized as a Value<T> , the output() function from Value<T> is used rather than from ValueBase . If you used normal variables instead of pointers, the output() function from ValueBase would be used, and we lose the information from the derived class.

Second, and this is related to the first one, you need to reference the pointer you get when using the value. If you want to output a ValueBase or Value<T> pointer with std::cout , you need to do it as std::cout << *var to output the contained value. If you just did std::cout << var , you would correctly get the address of the pointer instead.

I'm sure there are other options, especially when using Boost , but I'm no expert on that. Someone else might have more valuable information on that.

Other than that, what you are doing sounds like an act of laziness. C++ has a strongly typed system for a reason; not only is it well-defined, but you also know what to expect from your code. If you start making things fuzzy and use arbitrary container objects for all kinds of tasks, your code will lose readability, clarity, and will (most probably) produce numerous bugs that are very difficult to trace, debug, and in the end fix, because you need to support all the fancy containers you introduced to keep your framework running.

If you want to use a language like Java, its better to use Java instead of C++.

You can't do that.

Without knowing beforehand what type you're storing, you can't decide how much space you need to store it.

What you actually did in Java was not what you're asking for in C++, but more akin to std::map<KeyType, shared_ptr<void>> , and then doing a whole mess of dynamic_cast s on the held pointer, given some virtual interface Object from which everything (including int , float and char ) derive.

In C++ instead of deriving and using dynamic_cast , you could use reinterpret_cast and keep some kind of collection recording the type of every object you put in the collection.

This is "reflection", and C++ doesn't have that built in yet.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM