Exposing object sequences via public data interfaces (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 166
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

Exposing object sequences via public data interfaces (C++)

Post by fips »

More and more often, I design my code around object sequences rather than around individual objects. Having a single interface that manages the whole sequence brings a lot of flexibility and optimization opportunities on the implementation side. That’s exactly where the usual OOP methodology fails as all the common OOP idioms deal with 1-1 object relations and blindly insist on per-object encapsulation.

So it’s time to move a bit towards DOD. Here, I’m going to show a table idiom I’ve been using quite often recently. The idea is that the table holds all the objects in a sequence indirectly addressed via index (or generally via handle). There’s no per-object interface, objects are manipulated through table’s interface using a bunch of setters. Each object consists of a public and private state. Both states are managed and kept in sync internally via the table. The best thing is that the full object’s public state can be obtained by a single get() call that returns a const reference to the state struct, this call is essentially for free and can be easily inlined. All the bookkeeping is atomically done in setters. See the example below to get the idea:

VIEW THE CODE BELOW IN FULL-SCREEN (public_data_interface_sample.cpp)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=727
*/

#include <vector>
#include <string>
#include <cstdio>
#include <cassert>

/// Represents a read-only entity state (a kind of public data interface).
struct Entity
{
    const char *name;
    int x, y;

    Entity(const char *name, int x, int y) : name(name), x(x), y(y) {}
};

/// Entity table with just a single getter and multiple setters.
class Entity_table
{
 public:

    /// Adds a new entity, returns its index.
    size_t add(const char *name, int x, int y)
    {
        assert(name);
        _records.emplace_back(Entity_record(Entity_data(name, x, y)));
        return _records.size() - 1;
    }

    /// Returns the size of the table.
    size_t size() const { return _records.size(); }

    /// Returns an existing entity (its whole read-only state).
    const Entity & get(size_t index) const
    {
        assert(index < _records.size());
        return _records[index].entity;
    }

    /// Sets entity name.
    void set_name(size_t index, const char *name)
    {
        assert(index < _records.size());
        assert(name);
        const Entity_data &orig_data = _records[index].data;
        _records[index] = Entity_record(Entity_data(name, orig_data.x, orig_data.y));
    }

    /// Sets entity position.
    void set_position(size_t index, int x, int y)
    {
        assert(index < _records.size());
        const Entity_data &orig_data = _records[index].data;
        _records[index] = Entity_record(Entity_data(orig_data.name, x, y));
    }

 private:
    
    /// Represents a private entity state (data holder). 
    struct Entity_data
    {
        std::string name;
        int x, y;
        // here might come more implementation details that are not exposed in Entity

        Entity_data(const std::string &name, int x, int y) : name(name), x(x), y(y) {}
    };

    /// Contains entity and its data, it's responsible for keeping them linked and in sync.
    struct Entity_record
    {
        Entity_data data;
        Entity entity;

        /// Initializes data, links entity to the data.
        Entity_record(const Entity_data &data):
        data(data),
        entity(data.name.c_str(), data.x, data.y)
        {}

        /// Copies data, links entity to the new data.
        Entity_record(const Entity_record &rhs):
        data(rhs.data),
        entity(data.name.c_str(), data.x, data.y)
        {}

        /// Copies data, links entity to the new data.
        Entity_record & operator = (const Entity_record &rhs)
        {
            data = rhs.data;
            entity = Entity(data.name.c_str(), data.x, data.y);
            return *this;
        }
    };

    std::vector<Entity_record> _records;
};

int main()
{
    Entity_table entities;

    entities.add("foo", 10, 20);
    entities.add("bar", 30, 40);

    for(size_t i = 0, n = entities.size(); i < n; ++i)
    {
        const Entity &e = entities.get(i); // atomic getter
        printf("entity[%u] : name='%s', pos=[%d, %d]\n", unsigned(i), e.name, int(e.x), int(e.y));
    }

    // fine grained setters
    entities.set_name(0, "FOO");
    entities.set_position(0, 50, 60);
    entities.set_name(1, "BAR");
    entities.set_position(1, 70, 80);

    for(size_t i = 0, n = entities.size(); i < n; ++i)
    {
        const Entity &e = entities.get(i); // atomic getter
        printf("entity[%u] : name='%s', pos=[%d, %d]\n", unsigned(i), e.name, int(e.x), int(e.y));
    }

    return 0;
}

// output:
// entity[0] : name='foo', pos=[10, 20]
// entity[1] : name='bar', pos=[30, 40]
// entity[0] : name='FOO', pos=[50, 60]
// entity[1] : name='BAR', pos=[70, 80]