2

I use global maps to register one or several objects of the same type. I started with using a global namespace for that purpose.

Take this for an example (code untested, just an example):

//frame.h
class frame
{
public:
  frame(int id);
  ~frame();
};

namespace frame_globals
{
  extern std::map<int, frame *> global_frameMap;
};
//frame.cpp
#include "frame.h"
namespace frame_globals
{
  std::map<int, frame *> global_frameMap;
}

frame::frame(int id)
{ 
  //[...]
  //assuming this class is exclusively used with "new"!
  frame_globals::global_frameMap[id] = this;
}

frame::~frame()
{
  frame_globals::global_frameMap.erase(id);
}

This was a rather quick solution I used, but now I stumble again on the use case, where I need my objects registered and I asked myself if there was no better way, than using a global variable (I would like to get rid of that).

[EDIT: Seems to be incorrect] A static member variable is (to my best knowledge) no option, because a static is inline in every object and I need it across all objects.

What is the best way to do this? Using a common parent comes to mind, but I want easy access to my frame class. How would you solve it?

Or do you know of a better way to register an object, so that I can get a pointer to it from somewhere else? So I might not exlusively need "new" and save "this"?

Natulux
  • 187
  • 1
  • 1
  • 11
  • If you need it across multiple objects of independent classes, making the map a global variable (but inside a namespace, as you do) is probably the easiest and simplest solution). Another alternative is to have each object store a reference to the map inside itself, and pass that reference to the constructor, but that's not really as simple as the global variable. – Some programmer dude Jan 23 '20 at 07:36
  • I just thought about something like a std::shared_ptr, so that the map can get created with the constructor of the first object and erased with the deletion of the last object. But I can't get to a working thing in my mind. C++11 (or C++17) is still rather new to me and I was hoping for some new possibilities I'm not aware of. – Natulux Jan 23 '20 at 07:44
  • Even with a shared pointer, you still need someway for the objects to access this shared pointer, possibly by passing it to each and every object you create (like the second alternative in my previous comment). – Some programmer dude Jan 23 '20 at 07:48
  • 2
    Maybe I am missing something, but I really don't see a problem with making the map a static data member of `frame`. – n314159 Jan 23 '20 at 07:49
  • 2
    I don't understand why a `static` data member is a "no option". A `static` data member exists only once (shared by everyone) and does not need any instance of the class. It would do the same as a global variable. – Fareanor Jan 23 '20 at 07:51
  • Btw, I really hope your real code also overwrites the other constructors and assignment operators. Otherwise BAD things will happen. – n314159 Jan 23 '20 at 07:52
  • I guess I confused something with the statics. I tried to find what I had in mind, but couldn't... ;-) So maybe even use a static map like suggested in this article: [https://stackoverflow.com/questions/24248501/c-static-members-multiple-objects](https://stackoverflow.com/questions/24248501/c-static-members-multiple-objects) @n314159 I do not always overwrite all constructors, only if I need to. What BAD things did you have in mind here? – Natulux Jan 23 '20 at 08:07
  • You should really follow the [rule of 0/3/5](https://stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c11). What could happen with the above: You create `frame a(0);`, then copy it to `frame b = a` and `b` outlives `a`. So the destructor of `a` gets called and there is no entry to for `0` in the map anymore but there still lives an object with this `id`. For problems of this kind this is still relatively harmless but it kind of defeasts the purpose of what you want to do. – n314159 Jan 23 '20 at 08:14
  • You can try to use [C++ Singleton design pattern](https://stackoverflow.com/questions/1008019/c-singleton-design-pattern) – Yeheshuah Jan 23 '20 at 08:15
  • @n314159 Thanks for pointing that out. You are right, I should really overwrite at least the copy constructor. – Natulux Jan 23 '20 at 09:42
  • I don't quite understand the problem you're facing, in particular "A static member variable is (to my best knowledge) no option, because a static is inline in every object and I need it across all objects.", what do you mean by 'a static is inline in every object'? IMHO, having the map as static class variable is fine. – SPD Jan 23 '20 at 10:10
  • @SPD What I had in mind is that a static variable results in a local copy for each .cpp file like pointed out in the answer of this article: [https://stackoverflow.com/questions/14349877/static-global-variables-in-c](https://stackoverflow.com/questions/14349877/static-global-variables-in-c) Other than that: No problem really, I just didn't want the map to be global if I wouldn't need it to. – Natulux Jan 23 '20 at 10:35
  • @natulux, the link you referenced is talking about static variable, not static class variable (aka static member variable). They have different meanings. Have a look at https://en.cppreference.com/w/cpp/language/static. In short, a static class variable has single global storage, it satisfies your described use case. – SPD Jan 23 '20 at 10:47

3 Answers3

1

I would consider reversing the design. eg. what is wrong with using language built functionality ?

std::map<int, frame> frames_;

frames_.emplace(id);//adding, ie. creating frames

frames_.erase(id)//removing, ie. deleting frames

Now creating/deleting is as easy as before. Coincidently, if someone needs a frame for a different purpose there is no issue here. If frame is supposed to be polymorphic you could store std::unique_ptr instead.

Also I would consider making id a member of frame and then store in a std::set instead of std::map.

class frame
{
public:
  int id;
  frame(int id);

  friend bool operator <(const frame& lhs, const frame& rhs) {return lhs.id < rhs.id;}
};

std::set<frame> frames_;

Deleting is just:

frames_.erase(frame);

However in order to lookup based on id is now somewhat more complicated. Fortunately theres a solution at hand. That requires implementing a compare that is transparent, which among other things involves defining is_transparent.

Overall you need to think about ownership. Ask yourself: where or who would own/store the map/frame pointers/etc. Store it there. In case of shared ownership, use a shared_ptr (but use only this pattern when actually needed - which is more rare than what people use it for).


Some relevant core-guidelines

I.3 Avoid singletons

R.5 and R.6 Avoid non-const global variables

R.11 Avoid calling new and delete explicitly

R.20 and R.21 Prefer unique_ptr over shared_ptr unless you need to share ownership

darune
  • 10,480
  • 2
  • 24
  • 62
  • You raise some ideas here. I like the idea to create the frames by using emplace on the map. – Natulux Jan 23 '20 at 11:13
  • Can you shortly explain the operator you used? Why is it declared friend and what would you use it for? Or was it meant only as an example? – Natulux Jan 23 '20 at 11:42
  • @Natulux I try to answer it in generel way, since some details are left out - eg. are frames supposed to be polymorphic - i guess so, but you get the idea. More information about the problem could give a clearer answer. – darune Jan 23 '20 at 11:44
  • @Natulux it's for declaring it a free function inside the class scope. You can also just create it as a member function if preferred, then `bool operator <(const frame& rhs) const` – darune Jan 23 '20 at 11:47
0

You should ideally have a factory pattern to create a delete frame objects. The constructor of the frame must not be managing the map. It's like citizens are manging their passports (ideally govt does that for citizens). Have a manager class to keep frame in map (std::map<int,frame>). A method provided by the manager class would create the object:

class FrameManager
{
    std::map<int,frame> Frames;
public:
    frame CreateFrame(int id) 
    {
           frame new_frame(id);
           Frames[id] =  new_frame;
    }      
};

What about removals? Well, depending on your design and requirement, you can have either/both of:

  • Removal of frames by the destructor of the frame. It may call FrameManager::Remove(id);
  • Allow removal also by FrameManager only (FrameManager::Remove(id)). I'd choose this (see more below).

Now, note that with this approach, many objects so frame will get created (local, assignment to map, return etc.). You may use shared_ptr<frame> as the return type from CreateFrame, and keep shared_ptr as the type of map map<int, shared_ptr<frame>>. It may seem complicated to use shared/unique_ptr but they become really helpful. You don't need to manage the lifetime. You may pass same shared_ptr<frame> to multiple functions, without creating frame object multiple times.

Modified version:

class FrameManager
{
    std::map<int, shared_ptr<frame>> Frames;
public:
    shared_ptr<frame> CreateFrame(int id) 
    {
        if(Frames.count(id)>0) return nullptr;  // Caller can check shared_ptr state

        shared_ptr<frame> new_frame = make_shared<frame>(id);
        Frames[id] =  new_frame;

        return new_frame;
    }      
};

Using a frame-manager will allow complete control over frames creation, better error handling, safe/secure code, safe multithreaded code.

Ajay
  • 18,086
  • 12
  • 59
  • 105
  • I do not see what functionality the FrameManager is providing expect an extra level of obfuscation ? as it is OP could just use std::map (there is nothing wrong with that from the start) directly.. - care to explain ? – darune Jan 23 '20 at 12:51
  • @darune I didn't ask the OP to (must) follow this. I just gave my approach. Yes, it is a bit involved, and maybe over-engineering. Not good for small usage, but may become useful for larger use cases. – Ajay Jan 24 '20 at 06:04
0

I would like to put few points base on code posted by you.

  1. How some one will know about frame_globals::global_frameMap and what it does with out looking source code.
  2. If it is a global object then frame class logic can be bypassed and global object can be modified from outside.
  3. What if someone wants to mange frame objects in different way or if in future some requirement changes for example if you want to allow duplicate frames then you have to change frame object.
  4. frame class is not only having frame related data member and member function but also have managing logic. It seems not following single responsibility principle.
  5. What about copy/move constructor? std::map/std::set will affect constructors of frame class.

I think logic separation is required between frame and how to manage frames.I have following solution ( I am not aware of all use cases, my solution is based on posted code)

frame class

  1. frame class should not have any logic which is not related to frame.
  2. If frame object need to know about other frame objects then a reference of ContainerWrapper should be provided (not directly std::map or std::set, so that change will not impact frame)

ContainerWrapper class

  1. ContainerWrapper should manage all the frame objects.
  2. It can have interface as per the requirement, for example try_emplace which returns iterator of inserted element (similar to std::map::try_emplace), so rather then first creating frame object and then try to insert in std::map, it can check the existence of the id(key) and only create frame object if std::map doesn't have any object with specified id.

About std::map/std::set,
std::set can be used if frame object once created is not required to modify because std::set will not let you modify frame without taking it out and re-inserting it or you have to use smart pointer.

By this way a container object can be shared across multiple frame objects and doesn't have to be global object.

Vikas Awadhiya
  • 290
  • 1
  • 8