1

I have a class UISceneParams and I use it to store some parameters that I want to send over to a UIScene. It contains a unordered_map of shared_ptrs to DataParam instances. The problem is that my DataParam must use a template and feature multiple data types.

Is there any way I can make the unordered_map use multiple data types of DataParams?

I currently call this like this:

UISceneParams p;
p.AddParam<int>("test_value", 123);
p.AddParam<int>("test_value2", 456);

and in my scene I get the parameters using the GetParam<T> method, like this:

int value1 = p.GetParam<int>("test_value", 0);
int value2 = p.GetParam<int>("test_value2", 0);

I've been trying many ways to circumvent the errors, but I am a beginner with templates in C++. Can someone get me on the right track?

UISceneParams.h

#pragma once
#include <string>
#include <unordered_map>
#include <memory>

using namespace std;

template <typename T>
class DataParam {
public:
    DataParam(string t, T v) {
        _tag = t;
        _val = v;
    }

    T Value() const {
        return _val;
    }

    string Tag() const {
        return _tag;
    }
private:
    string _tag;
    T _val;
};

class UISceneParams {
public:
    bool HasParam(string tag) {
        return params.find(tag) != params.end();
    }

    template <typename T>
    void AddParam(string tag, T value) {
        if (HasParam(tag)) params.erase(tag);
        params.insert({ tag, make_shared<DataParam>(tag, value) });
    }

    template <typename T>
    T GetParam(string tag, T def_value) {
        if (!HasParam(tag)) return def_value;
        auto p = params.find(tag)->second;
        return p->Value;
    }
private:
    unordered_map<string, shared_ptr<DataParam>> params;
};

Edit: Final code

#pragma once
#include <string>
#include <unordered_map>
#include <memory>

using namespace std;

class IDataParam
{
public:
    virtual ~IDataParam() = default;
};

template <typename T>
class DataParam : public IDataParam {
public:
    DataParam(string t, T v) {
        _tag = t;
        _val = v;
    }

    T Value() const {
        return _val;
    }

    string Tag() const {
        return _tag;
    }
private:
    string _tag;
    T _val;
};

class UISceneParams {
public:
    bool HasParam(string tag) {
        return params.find(tag.data()) != params.end();
    }

    template <typename T>
    void AddParam(string tag, T value) {
        if (HasParam(tag)) params.erase(tag);
        params.insert({ tag, make_shared<DataParam<T>>(tag, value) });
    }

    template <typename T>
    T GetParam(string tag, T def_value) {
        if (!HasParam(tag)) return def_value;
        auto p = dynamic_cast<const DataParam<T> &>(*params.find(tag)->second);
        return p.Value();
    }
private:
    unordered_map<string, shared_ptr<IDataParam>> params;
};
r0neko
  • 15
  • 6
  • 1
    Could you use `std::unordered_map>` ? – Ghasem Ramezani Aug 20 '21 at 11:24
  • Are you going for the functionality of [`std::any`](https://en.cppreference.com/w/cpp/utility/any), or do want to restrict the possible types, more like [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant)? – JaMiT Aug 20 '21 at 11:25
  • JaMiT: I want to go for the functionality of `std::any`. I will test shortly and see if that helps. – r0neko Aug 20 '21 at 11:27
  • When I get the value, I'm supposed to cast it to `DataParam` or `DataParam`? – r0neko Aug 20 '21 at 11:32
  • @r0neko Since `DataParam` is not a type, you cannot cast anything to `DataParam`. (This is basically the same problem you have with your initial approach. Since `DataParam` is not a type, you cannot have a (smart) pointer to `DataParam`.) You do not get a type until you supply a template argument, as in `DataParam`. – JaMiT Aug 20 '21 at 13:31

1 Answers1

3

You could make your DataParam inherit from a marker interface like IDataParam, where the interface is just like

class IDataParam
{
    virtual ~IDataParam() = default;
};

You could then store all your params as unordered_map<string, shared_ptr<IDataParam>> since they all have the same base type.

Then when getting the param, you just get the IDataParam first by the string key. And then you do a static_cast<const DataParam<T> &>(*gotParam) for example to cast it down to the correct templated DataParam type before returning.

Of course this would not work if you have multiple params with the same key but different T, of you might also run into trouble if you get a param as a type that it is not, to circumvent this issue you could use dynamic_cast though

Tack D.
  • 157
  • 1
  • 6
  • In case of downcasting I would go for dynamic_cast not static_cast. https://stackoverflow.com/questions/47310700/c-static-cast-downcast-validity – Pepijn Kramer Aug 20 '21 at 11:50
  • Thanks for the advice. I implemented it and it works now! – r0neko Aug 20 '21 at 11:54
  • Since the definition of `_tag` does not depend on the template parameter, you might want to put `_tag` in the parent class (especially if -- and this is just me guessing, but if -- `_tag`'s job is to say which type is stored in `_val`). Or maybe drop the `_tag` data member and instead make the `Tag()` function virtual, and each derived class can override it. That might give better type safety. – JaMiT Aug 20 '21 at 13:35
  • the `Tag` is just an identifier of the `DataParam`. – r0neko Aug 20 '21 at 18:35