user3689034
user3689034

Reputation: 79

Create a function that returns different types with existing class inheritance

So there are the following existing classes:

class Data {};

class String_data : public Data {
  string m_data;
    public:
  string str() { return m_data; }
};

class Integer_data : public Data {
  int m_data;
    public:
  int value() { return m_data; }
};

I am trying to do something like overloaded functions but for the base class:

string get_data(String_data *data) {
  return data->str();
}

int get_data(Integer_data *data) {
  return data->value();
}

The problem is that these objects are stored in a container as an object pointer of the base class like:

std::map<string, Data*> data_list;
data_list.emplace( std::pair<string, Data*>("first", new String_data()) );
data_list.emplace( std::pair<string, Data*>("second", new Integer_data()) );

So, i am trying to get it to work something like this:

string first_data = get_value(data_list["first"]);
int second_data = get_value(data_list["second"]);

Edit: Sorry this will have to get long. I wanted to keep the question simple but it won't quite work unless i state the full intention.

Some of the items in the data_list are pretty constant and i wanted to create an easy way to expand on the list of items without having to create a wrapper class and have separate access method for each of it like this:

class DataListWrapper {
  map<string, Data*> m_data_list;
public:
  string get_house() { return ((String_data*)m_data_list["house"])->str(); }
  int get_loan() { return ((Integer_data*)m_data_list["loan"])->value(); }
  // etc...
};

To make sure that these the developer won't accidentally mistype the string keys i created global constants for the compiler to check in compile-time.

#ifndef STRING_CONSTANTS
#define STRING_CONSTANTS
constexpr char c_house[] = "house";
constexpr char c_loan[] = "loan";
#endif

So, at the moment we do this:

string house = ((String_data*)m_data_list["house"])->str();
int loan = ((Integer_data*)m_data_list["loan"])->value();
call_me(house, loan);

But I want to expand on the list and easily get the value by just doing very simple and effortless something like:

string house = get_value(m_data_list[c_house]);
int loan = get_value(m_data_list[c_loan]);
call_me(house, loan)

Edit: In some sense i wanted to do it without type-casting as it gets really verbose and kind of blinds people away from what the code is trying to do with all the type-casting going on.

Upvotes: 2

Views: 89

Answers (1)

Guillaume Racicot
Guillaume Racicot

Reputation: 41750

You can achieve that easily with std::variant, which keeps arount metadata that tells which type is used. So you can remove the empty base class:

using Data = std::variant<String_data, Integer_data>;

std::map<string, Data> data_list;

data_list.emplace("first", String_data{});
data_list.emplace("second", Integer_data{});

Then use a visitor that let you inspect the underlying data:

auto get_data(String_data const& data) -> std::string {
    return data.str();
}

auto get_data(Integer_data const& data) -> int {
    return data.value();
}

std::visit(
    [](auto& data) { auto value = get_data(data); },
    data_list["first"]
);

std::visit(
    [](auto& data) { auto value = get_data(data); },
    data_list["second"]
);

However, if you want to keep the class hierarchy, you can always implement your own visitor using virtual polymorphism:

struct Visitor {
    virtual void operator()(int const&) const = 0;
    virtual void operator()(std::string const&) const = 0;
};

struct PrintVisitor : Visitor {
    void operator()(int const& value) const override {
        std::cout << value;
    }

    void operator()(std::string const& value) const override {
        std::cout << value;
    }
};

struct Data {
    virtual void visit(Visitor const&) const = 0;
};

struct String_data : Data {
    void visit(Visitor const& v) const override {
        v(m_data);
    }

private:
    std::string m_data;
};

struct Integer_data : Data {
    void visit(Visitor const& v) const override {
        v(m_data);
    }

private:
    int m_data;
};

Then use it:

data_list["first"]->visit(PrintVisitor{});
data_list["second"]->visit(PrintVisitor{});

Of course, if you already know the types of your elements in the map, you can downcast them using static_cast. But it requires that you have enough context to know in advance which type there will be:

string first_data = get_value(static_cast<String_data*>((data_list["first"]));
int second_data = get_value(static_cast<Integer_data*>((data_list["second"]));

call_me(first_data, second_data);

I would advise against using dynamic_cast. When you are about to use dynamic casts, you better off using a variant. Downcast it that way imply you know the amount of types you'll deal with, and will constrain the hierarchy just like a variant, but in a more subtle and error prone way.


Since you added more detail to the question, the problem is clearer.

My solution would be to not use strings directy, and use some kind of identifier to encapsulate both how to cast and how to get the value.

struct house_data_t {
    static constexpr auto index = "house";
    using type = String_data; 
} inline constexpr house_data{};

struct loan_data_t {
    static constexpr auto index = "loan";
    using type = Integer_data; 
} inline constexpr loan_data{};

Then make a function that uses this metadata:

std::string house = get_value(house_data, data_list);
int loan = get_value(loan_data, data_list);

The get_value function can be implemented like that:

auto get_data(String_data* data) -> std::string {
    return data->str();
}

auto get_data(Integer_data* data) -> int {
    return data->value();
}

using data_list_t = std::map<std::string_view, std::unique_ptr<Data>>;


template<typename data_t, typename data_type_t = typename data_t::type>
auto get_value(data_t data, data_list_t& data_list) -> decltype(get_data(std::declval<data_type_t*>())) {
    return get_data(static_cast<data_type_t*>(data_list[data.index].get()));
}

Live example

Upvotes: 5

Related Questions