Optional user data members?

Hello,

I'm trying to write a game engine as a library that another application can link to. In the library, there's a class called "Entity".

I'd like be able to attach some userdata to instances of this class through the application linked with the library.

I tried subclassing Entity, but I found that this leads to loads of spaghetti code in my project.

Currently, I'm doing it this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Entity : public btMotionState{
 
// Other stuff...

    std::map<std::string, float> userFloats;
    std::map<std::string, int> userInts;
    std::map<std::string, std::string> userStrings;
    
    inline float& getUserFloat(const std::string& key){ return userFloats[key]; }
    inline int& getUserInt(const std::string& key){ return userInts[key]; }
    inline std::string& getUserString(const std::string& key){ return userStrings[key]; }
    
    inline void setUserFloat(const std::string& key, float value){ userFloats[key] = value; }
    inline void setUserInt(const std::string& key, float value){ userInts[key] = value; }
    inline void setUserString(const std::string& key, float value){ userStrings[key] = value; }
};


It works, but I found that on my system this makes the Entity class's size explode to 352 bytes. There will be hundreds of entities, so this is way too big for a class that's supposed to be lightweight.

Most instances of the class don't have any userdata, so that's mostly wasted memory.

What are my options here?

I guess I could make the maps into pointers and work with new/delete, so only the instances that need userdata could have the maps allocated. Could that be the way to go?
Last edited on
I have no idea what you are trying to accomplish
<nolyc> You're trying to do X, and you thought of solution Y. So you're asking about solution Y, without even mentioning X. The problem is, there might be a better solution, but we can't know that unless you describe what X is.

> I tried subclassing Entity, but I found that this leads to loads of spaghetti code
If it has an `is a' relationship, only then you may consider subclassing

> Most instances of the class don't have any userdata
┬┐what's the purpose of `userdata'?
If some instances would not use it, then it may be a pointer.

> I guess I could make the maps into pointers and work with new/delete
maps are just a couple of pointers
Basically, I have a game world which is mainly a collection of instances of this Entity class.

Entities have a position, rotation, physics properties and a pointer to a 3d model, and this is how it's defined in the library.

In the application that links to the library, it might be useful for some of the entities to contain some extra data (which I'm calling user-data here) such as the name of a character, the time since it last drank some magic potion, the location of their house, the item they might be holding in their hand etc...

Since the Entity class is defined in the library (and intended to be reused in other projects), these cannot be added as class members directly, and by far not all entities will need these properties.
> a class that's supposed to be lightweight.
> Most instances of the class don't have any userdata, so that's mostly wasted memory.
> What are my options here?

The solution that jumps to the mind immediately, is to have the objects hold extrinsic state (in a shared lookup table). The object size is not affected at all; and for most instances (do not have any user data), there is zero memory overhead.

There are two issues with the existing code:
float& getUserFloat(const std::string& key){ return userFloats[key]; }

One, it will needlessly burn memory if the object does not have any user data (or if user data with this key does not exist). Ideally, use find() instead of the subscript operator.

Two, it is not const-correct.

Something along these lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <string>
#include <map>

class Entity  : public btMotionState
{

    // ...
    class user_stuff // userdata to be attached to instances
    {
        std::map<std::string, float> userFloats;
        std::map<std::string, int> userInts;
        std::map<std::string, std::string> userStrings;
        friend class Entity ;
    };
    // shared lookup table mappimg object instances to attached user data.
    static std::map< const Entity*, user_stuff > user_data_map ;

    user_stuff* user_data() // retrieve user_stuff for this object
    {
        auto iter = user_data_map.find(this) ;
        return iter != user_data_map.end() ? &iter->second : nullptr ;
    }

    const user_stuff* user_data() const // retrieve user_stuff for this object
    {
        auto iter = user_data_map.find(this) ;
        return iter != user_data_map.end() ? &iter->second : nullptr ;
    }

    public:

        // destructor removes the attached user_stuff entries
        ~Entity()
        {
            auto iter = user_data_map.find(this) ;
            if( iter != user_data_map.end() ) user_data_map.erase(iter) ;
        }

        // TODO:
        // either make entity non-copyable and non-moveable
        // or
        // copy constructor - copy user_stuff
        // move constructor - move user_stuff
        // copy assignment - copy assign user_stuff
        // move assignment - move assign user_stuff


        float user_float( const std::string& key ) const
        {
            auto p = user_data() ;
            if(p) // if there is user data attached to this object
            {
               auto iter = p->userFloats.find(key) ; // look up the key
               if( iter != p->userFloats.end() ) return iter->second ;
            }
            return 0 ; // not found
        }
        // likewise for user_int, user_string inspectors

        // when we insert data, we create the the entries if needed
        void user_float( const std::string& key, float value )
        { user_data_map[this].userFloats[key] = value ; }
        // likewise for user_int, user_string modifiers
};

// in the implementation file
std::map< const Entity*, Entity::user_stuff > Entity::user_data_map ;
> In the application that links to the library,
> it might be useful for some of the entities to contain some extra data (which I'm calling user-data here)
> Since the Entity class is defined in the library,
> these cannot be added as class members directly
1
2
3
4
class thing_with_a_name{
   std::string name;
   Entity all_the_other_stuff;
};
Topic archived. No new replies allowed.