2
\$\begingroup\$

I created a utility class to keep track of all objects inheriting from it. My main motivation here was to have a clean solution to select which entities in a multiplayer game are synced with the server. I tried to make it as hard as possible to make mistakes (eg. inherit from the wrong crtp parameter) and keep things encapsulated. Well, code speaks more than a thousand words so here we go:

#include <unordered_set>    

template<typename t_derived, std::size_t reserved_size = 0>
struct tracker_t
{
    friend t_derived;

private:    // protected:
    tracker_t()
    {
        data::instances().insert(static_cast<t_derived*>(this));

        if constexpr(reserved_size > 0)
        {
            // force initialization of data::s_initialized
            (void)data::s_initialized;
        }
    }

    tracker_t(const tracker_t&)
    {
        data::instances().insert(static_cast<t_derived*>(this));
    }

    ~tracker_t()
    {
        data::instances().erase(static_cast<t_derived*>(this));
    }

    template<typename t_func>
    static void iterate(t_func func)
    {
        for(auto it : data::instances())
        {
            func(it);
        }
    }

    // forward unordered_set methods

    static void reserve(std::size_t n)
    {
        data::instances().reserve(n);
    }

    static bool empty()
    {
        return data::instances().empty();
    }

    static std::size_t size()
    {
        return data::instances().size();
    }

    static auto begin()
    {
        return data::instances().begin();
    }

    static auto end()
    {
        return data::instances().end();
    }

    static auto cbegin()
    {
        return data::instances().cbegin();
    }

    static auto cend()
    {
        return data::instances().cend();
    }

    static typename std::unordered_set<t_derived*>::iterator find(const t_derived* item)
    {
        return data::instances().find(item);
    }

    struct data // private:
    {
        friend tracker_t;

    private:
        static bool reserve()
        {
            instances().reserve(reserved_size);
            return true;
        }

        static std::unordered_set<t_derived*>& instances()
        {
            static std::unordered_set<t_derived*> s_instances;
            return s_instances;
        }

        static const bool s_initialized;
    };
};

// force execution of reserve()
template<typename t_derived, std::size_t reserved_size>
const bool tracker_t<t_derived, reserved_size>::data::s_initialized {tracker_t<t_derived, reserved_size>::data::reserve()};

A small example:

#include <iostream>
#include <string>

struct animal_t : tracker_t<animal_t>
{
    using tracker_t::iterate;
    using tracker_t::size;

    virtual std::string get_name() const = 0;
};

struct pig_t : animal_t
{
    std::string get_name() const override
    {
        return "Pig";
    }
};

struct duck_t : animal_t
{
    std::string get_name() const override
    {
        return "Duck";
    }
};

struct guinea_pig_t : pig_t
{
    std::string get_name() const override
    {
        return "Guinea Pig";
    }
};


int main()
{
    duck_t duck1, duck2;
    pig_t pig1, pig2;
    guinea_pig_t guinea1;

    std::cout << "Number of objects deriving from animal_t: " << animal_t::size() << "\n\n";

    animal_t::iterate([](animal_t* animal)
    {
        std::cout << animal << ' ' << animal->get_name() << '\n';
    });

    return 0;
}

I'd like to get some feedback on:

  • is there anything broken (ub, performance, ...)

  • best practices

  • error susceptibility

  • code style

  • suggestions on how to make it easier to use

\$\endgroup\$
0
\$\begingroup\$

Creation of the static map

What immediately cought my eye were these lines:

    if constexpr(reserved_size > 0)
    {
        // force initialization of data::s_initialized
        (void)data::s_initialized;
    }

and of course these:

// force execution of reserve()
template<typename t_derived, std::size_t reserved_size>
const bool tracker_t<t_derived, reserved_size>::data::s_initialized {tracker_t<t_derived, reserved_size>::data::reserve()};

I get it, but it is a hack. Wouldn't it be better to just create custom set derived from std::unordered_set that will handle the reserve in its constructor? You won't even need all those static auto begin() (that appear to be unused, but I see the commented-out protected:).

template<typename t_derived, std::size_t reserved_size = 0>
struct tracker_t {
    struct instances_t: protected std::unordered_set<t_derived*> {
        using base = std::unordered_set<t_derived*>;
        using base::reserve;
        using base::insert;
        using base::erase;
        using base::size;
        using base::begin;
        using base::end;
        using base::iterator;
        instances_t() {
            if constexpr (reserved_size > 0)
                reserve(reserved_size);
        }
    };
    static inline instances_t instances;
    tracker_t()                 { instances.insert(static_cast<t_derived*>(this)); }
    tracker_t(const tracker_t&) { instances.insert(static_cast<t_derived*>(this)); }
    ~tracker_t()                { instances.erase(static_cast<t_derived*>(this)); }

    template<typename t_func>
    static void iterate(t_func func) {
        for (auto it : instances)
            func(it);
    }
};

If you wonder why I used protected-inheritance (could as well be private) and bunch of using's, then it is because unordered_set has public non-virtual destructor ;) Feel free to use composition, if you wish (some prefer it).

And static inline is C++17, I believe, more modern way than using the static method returning static local.

\$\endgroup\$
  • \$\begingroup\$ I declare t_derived as friend. As a result it has access to the unordered_set forwards. They aren't meant to be used inside this class. I haven't used static inline variables yet but I'll have a look at them. Inheriting unordered_set would simplify a lot, could catch! \$\endgroup\$ – HenrikS Oct 10 '18 at 14:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.