0

I would like to have a function that accepts a std::map containing a (pointer to a) base class, but I can't get it to work. Here is my minimal and simplified example:

struct Entity {
  int i;

  Entity(const int i_) : i(i_) {}
  virtual ~Entity() {}
};

struct A : public Entity {
  bool a;
  A(const int i_, const bool a_) : Entity(i_), a(a_) {}
};

void efunc(const std::map<int, std::shared_ptr<Entity>>& m) {
  for (auto& it : m) {
    std::cout << it.second->i << std::endl;
  }
}

int main() {
  std::map<int, std::shared_ptr<A>> aMap;
  std::shared_ptr<A> myA = std::make_shared<A>(1, true);
  aMap.insert({myA->i, myA});

  // efunc(aMap);  // DOES NOT WORK
}

Obviously simply passing the map this way is not allowed, but how can I make it work?

As a bonus, how would I detect the type in the map so that I can execute code specific to that subclass?

Thank you in advance!

Update: Using templates seems to do the trick, this is what I'm using right now:

template <class T>
void tfunc(const std::map<int, std::shared_ptr<T>>& m) {
  for (auto& it : m) {
    std::cout << it.second->i << std::endl;
  }
}

It does feel a bit strange, since I loose IntelliSense and I don't understand why the compiler is not giving me an error. I feel like concepts might help (e.g. to make sure T inherits Entity), but this is beyond what I need right now.

Thank you again for all the responses!

Update 2: Alright, here it is using concepts to ensure that the template containst the member i:

template <class T>
concept hasI = requires(T a) { a.i; };

template <class T>
concept inheritsEntity = std::derived_from<T, Entity>;

template <hasI T>  
/// alternative: template<inheritsEntity T>
void tfunc(const std::map<int, std::shared_ptr<T>>& m) {
  for (auto& it : m) {
    std::cout << it.second->i << std::endl;
  }
}
Markstar
  • 639
  • 9
  • 24
  • You basically have three options: (1) change the signature of `efunc`, (2) change the type of `aMap`, (3) add a conversion (which will either perform a shallow copy or, if you write a suitable adapter type, performs no copy but translates on the fly). – Konrad Rudolph Jan 11 '23 at 08:45
  • 1
    To ask the obvious question: is making `efunc` a function template an option for you? – Konrad Rudolph Jan 11 '23 at 09:38
  • 1
    And to answer your second functions: by defining suitable virtual functions in the base class which are overridden with the specific functionality in the subclasses. – Konrad Rudolph Jan 11 '23 at 09:55
  • @KonradRudolph I have never used templates before. So beside templates making my head spin, is there another reason not to use them? I'm not sure how I would implement this here, though. :/ – Markstar Jan 11 '23 at 10:14
  • @HuyNhatTran Yeah, I think something like this is the best approach for this, as has been suggested (and then argued against) before. – Markstar Jan 11 '23 at 10:58
  • The good news: your code is *already* using templates (`std::map` is a template)! :-) — And you will *need* to learn to use templates if you want to use C++ effectively, so now is as good a time as ever. I see you managed to solve your problem, which is great! – Konrad Rudolph Jan 11 '23 at 11:14

3 Answers3

0

You can make aMap a std::map<int, std::shared_ptr<Entity>> and still hold shared pointers to instances of A.

In the code below myA which will be stored in the map is of type std::shared_ptr<Entity> but is initialized with a shared pointer to an instance of class A. This is OK because A is derived from Entity.

//----------------------------VVVVVV--------
std::map<int, std::shared_ptr<Entity>> aMap;

//--------------VVVVVV-------------------------V-----------
std::shared_ptr<Entity> myA = std::make_shared<A>(1, true);

aMap.insert({ myA->i, myA });

efunc(aMap);  // Works now.

As commented below, myA can also be std::shared_ptr<A> and still be stored in aMap:

std::shared_ptr<A> myA = std::make_shared<A>(1, true);

Alternatively, if using efunc is quite rare, you can keep aMap as std::map<int, std::shared_ptr<A>>.
Then whenever you need to call efunc, create a temporary std::map<int, std::shared_ptr<Entity>> containing a copy of the content of aMap and use it to call efunc.
The shared pointers will still refer to your original objects.
It's inefficient, but if you don't use efunc frequently it might be sufficient.

wohlstad
  • 12,661
  • 10
  • 26
  • 39
  • Thank you for your reply! Yes, except then i have to cast the the pointers of `myA` to the right type every time I use them elsewhere (which happens a lot more). By the way, when changing the map type to `std::map>`, adding a `std::shared_ptr` still works. – Markstar Jan 11 '23 at 08:39
  • It's not efficient, but if it happens rarely can solve your original issue by creating a temporary `std::map>` _copy_ from your `std::map>` just for calling `efunc`. – wohlstad Jan 11 '23 at 08:43
  • Creating another copy would be the easiest to write solution, but it would be called `O(n^2)` times, I would like to avoid it. – Markstar Jan 11 '23 at 08:53
  • 1
    I don't think there is an easy solution. Maybe something can be done but it requires deeper and broader understanding of your system. – wohlstad Jan 11 '23 at 09:02
0

To answer your first question, you could use template programming; the compiler will generate the code specific to the required type for you:

template <class EntityType> 
void efunc_template(const std::map<int, std::shared_ptr<EntityType>>& m) {
  for (auto& it : m) {
    std::cout << it.second->i << std::endl;
  }
}

The function can then be called in your main function using:

efunc_template<A>(aMap); 

Regarding your second question (how to execute code specific to your type), check can be done at compile time using:

template <class EntityType> 
void efunc_template(const std::map<int, std::shared_ptr<EntityType>>& m) {
  for (auto& it : m) {
    if constexpr (std::is_same_v<A, EntityType>) {
      std::cout << "Type is A" << std::endl;
    } else {
      std::cout << "Other type: " << typeid(EntityType).name() << std::endl;
    }
    std::cout << it.second->i << std::endl;
  }
}
GabrielGodefroy
  • 198
  • 1
  • 8
  • If we have a simple runtime polymorphic solution which uses virtual functions, there is really no need for using a template at all. This answer goes fully to the wrong direction! My two cents! – Klaus Jan 11 '23 at 10:04
  • 1
    Thanks, I think this is it. There seems to be a philosophical debate in this thread whether or not using templates for this is "bad practice", but I'm amazed how simple it is and does what I want. I have to admit I don't quite get why the compiler is not complaining, but I think I can live with that for now. :) – Markstar Jan 11 '23 at 10:57
  • As mentioned by @Klaus this solution is hackish as it mixes a runtime polymorphism- and template- based one. This solution does the job if you don't want to modify the `efunc` fonction calls. As suggested by @Konrad Rudolph, the `if constexpr` if also not so cleaned, and can be replaced using virtual functions. Have a look at the L-principle on the SOLID page (https://en.wikipedia.org/wiki/SOLID). Also, if you don't know about template, have a look at a good c++ book (https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) – GabrielGodefroy Jan 11 '23 at 12:40
-1

Change the declaration of your std::map variable from :

std::map<int, std::shared_ptr<A>> aMap;

to :

std::map<int, std::shared_ptr<Entity>> aMap;

This way, the map matches what's expected by your efunc function.

Using inheritance as you did, you can indeed insert in your aMap data structure instances of type Entity as well as instances whose the type derivates from Entity. This is the case for struct A.

To detect the type in the map, you can either use a scoped enum as member within the Entity struct, or using the typeid operator, which returns an instance of std::type_info, with which you can get the name of the type.

However, the returned name is not guaranteed to be the same across implementations.

======== output from Clang ========

myint has type: i

mystr has type: NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE

======== output from MSVC ========

myint has type: int

mystr has type: class std::basic_string<char,struct > std::char_traits,class std::allocator >

For this reason, you might want to use dynamic_cast instead, such as :

class Base {
    public:
        virtual ~Base() {}
};

class T1Base : public Base {};
class T2Base : public Base {};

template <typename T> class T1 : public T1Base {};
template <typename T> class T2 : public T2Base {};

int main()
{
    Base *b = find_by_name();

    if (dynamic_cast<T1Base*>(b))
        cout << "T1" << endl;
    else if (dynamic_cast<T2Base*>(b))
        cout << "T2" << endl;
    else
        cout << "unknown" << endl;

    delete b;

    return 0;
}
Issylin
  • 353
  • 2
  • 3
  • 11
  • 1
    When the answer is `dynamic_cast` or `typeid`, you are usually asking the wrong question. In particular, I don't see how this would help OP. – Konrad Rudolph Jan 11 '23 at 09:00
  • Thanks for replying and your example! Changing the map to `std::map>` means I have to cast the entries in all other occasions (which is the vast majority), so I'm not too fond of this and probably would just copy the function for the different inherited types (which is relatively small). Regarding the example: is there a reason to use templates instead of trying a `dynamic_cast` and see if it is null? – Markstar Jan 11 '23 at 09:42
  • 1
    @Markstar If you need to cast the *entities* (rather than the map) this indicates a flaw in your object design. In most cases you shouldn't need to cast within class hierarchies *at all* (I'm aware that there are exceptions but, well, these should be *exceptions*). The whole point of inheritance is that the client code can use the the implementations interchangeably via the base class interface. – Konrad Rudolph Jan 11 '23 at 09:52
  • dynamic_cast is a first hint for very bad design and in the given scenario not needed nor helpful. – Klaus Jan 11 '23 at 10:03
  • @KonradRudolph Hmm, I'm using this to import data from a csv file. The file may contain different kinds of objects, which I have thouroughly verify, cast, create implied objects, and a lot more before I can add the final object. While the objects may have some similarities (e.g. a name, though even there two diffent types of object may have the same name), they are totally different in other regards. So I don't see how I can make it so all objects implment the same set of virtual functions. – Markstar Jan 11 '23 at 10:27