Páginas

terça-feira, 22 de março de 2016

Games in C++ can't be that hard, right? - Handles: hey freak me out, I'm coding!

About fifteen days ago I've posted about smart pointers and ownership. After laying out the rules for a piece of the system I wanted to code – namely, a way to iterate over instances of a type, following some restrictions and demands – I ended up concluding that maybe I should try to use handles. With the little time I have to code side projects, this ended up being a heavier task than expected. Because of c++ stuff I didn't know, didn't remember, didn't understand, or any permutation of these, I had to throw way many attempts.

Soooooo… the truth is, things got too complex and I decided to fall back into KISS (Keep it Simple, Stupid). Planning a bigger scope set of rules and demands would take too much of the little time available, so I've used a set of rules for the containers themselves, with handles.

[r1] – Containers are the unique owners of the data stored there.
[r2] – Contained data must be created within the container itself.
[r3] – Handles to contained data are created only (and only when) with the storing of data.
[r4] – Handles must be encapsulated by smart pointers. They must know how to resolve their destruction, and of the data they point to.
[r5] – Iterators that "directly" point to stored data must be available. They are unsafe and should be used with caution.
[r6] – Data can be destroyed even when their handles are still alive. This will make the handles point to a specific "No Data" element.
[r7] – Containers must assume "non pointers" template data types.
[r8] – Containers must be inheritable from an abstract container.

I know some rules can be rather controversial, but I'll leave it as is for now. Actually, they'll force some restrictions on the stored data as well. The biggest problem probably comes from rule 8; there are reasons that I want a uniform representation of any possible container for the same data type, such as a list of containers to be used by factories.

A good name for the container might be StoringFactory. After researching for a while, it seems to be rather unusual to mix up a container together with a factory, as the separation in "container only" and "factory only" classes is desirable. However, for my needs, it seems to make more sense to simply merge them together, with a name to clearly show this mix of responsibilities. Am I deciding wrong? Don't know, but I'm really afraid of over-engineering this, right now...


 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
template <class DataType>
class StoringFactory
{
public:
    virtual ~StoringFactory() {}

    // Deletes the data pointed by a handle. The handle won't be discarded,
    // so any holding handle to the same data will still be "valid", and
    // return noDataPointer.
    virtual void DeleteData(StoringFactoryHandle<DataType>& handle) = 0;

    // Deletes the data pointed by a handle, and the handle itself. Using the
    // same holding handle after deleting it will result in undefined behavior.
    virtual void DeleteHandle(StoringFactoryHandle<DataType>& handle) = 0;
    virtual DataType* Get(StoringFactoryHandle<DataType>& handle) const = 0;

    virtual DataType* SetNoDataPointer(std::unique_ptr<DataType> noDataPointer) = 0;

    // Returns the number of registered elements in this container. The name is not
    // size as it could be misleading.
    virtual int ElementsCount() const = 0;

    // Iterators for ranged-for loops
    virtual StoringFactoryIterator<DataType> begin() const = 0;
    virtual StoringFactoryIterator<DataType> end() const = 0;
};


The abstract StoringFactory class above is rather simple. Someone that is somehow reading this post will probably notice that there are no methods to actually insert or create data into this container. The reason for that is that I want to try using emplacement methods with variadic templates. There's a catch, though: they can't be virtual, forcing them to be in the child classes. How, then, can I insert data into the container when I have only a pointer to the base class? It turns out that it is indeed possible, by means of abstract factories or dependency injections, and some magic.

Next comes the dubious methods of DeleteData and DeleteHandle. DeleteData is rather dangerous and nonsensical, but I want the ability to remove data from memory without first needing to properly clean up anything related to its handle (such as every object that has a shared ownership of the handle). This can be useful in some very rare cases, as unloading low priority resources when in need of memory. The DeleteHandle probably shouldn't be public. Handles will call this method to destroy themselves, but I don't see any reason to not make this method protected, and the handles a friend class of the StoringContainer.

And what does the handle looks like?


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using handle_type = unsigned int;

template <class DataType>
class StoringFactoryHandle
{
public:
    StoringFactoryHandle(handle_type handle, StoringFactory<DataType>* factory);

    inline DataType* Get() const;
    inline handle_type GetHandleValue() const; // TODO : restrict creation access



protected:
    StoringFactory<DataType>* m_Factory;
    handle_type m_Handle;
};


They're just a small container with enough information to refer back to the original data. I've restricted the handles to store handle_type (that is, unsigned int) as their "handling key". I could have used pointers, but ended up deciding on "indexes" or "keys" instead. This is a decision that'll probably be very hard to change later on... so let me pretend to be proud and sure of what I'm doing.

Finally, all that remain are the iterators types for the StoringFactory class. It is possible to make an abstract iterator type and then inherit as needed, for each storage desired, such as lists, maps, etc. This should work, by relying on Covariance, but soon many flaws of this idea would sprout everywhere. One of the problems, for example, is the need of casting between parent and child types of the iterator. Even if that is solved, templates and slicing would come right after with many other problems. The path I chose was to use the pimpl idiom, though I feel like I'm missing a bit of its point.


 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
template <class DataType>
class StoringFactoryIterator
{
public:
    StoringFactoryIterator(StoringFactoryIterator<DataType>& other);
    StoringFactoryIterator(std::unique_ptr<StoringFactoryIteratorImpl<DataType>> iteratorImplementation);
    ~StoringFactoryIterator() = default;

    DataType& operator *() const;
    const StoringFactoryIterator& operator ++();
    bool operator !=(const StoringFactoryIterator& other) const;



protected:
    std::unique_ptr<StoringFactoryIteratorImpl<DataType>> m_StoringFactoryIteratorImpl;
};



template <class DataType>
class StoringFactoryIteratorImpl
{
public:
    StoringFactoryIteratorImpl() {}
    virtual ~StoringFactoryIteratorImpl() {}

    virtual DataType& Dereference() const = 0;
    virtual const StoringFactoryIteratorImpl& PreIncrement() = 0;
    virtual bool IsDifferent(const StoringFactoryIteratorImpl& other) const = 0;

    virtual StoringFactoryIteratorImpl* clone() = 0;
};


The idea was to use a single iterator "interface" class to redirect operations to a real implementation, that is storage specific. Some problems with slicing still remains, such as making clones of the iterator implementation when the "interface" is copied around. Since the implementation is handled only by the "interface" class, methods such as clone are used. Casting is still a problem (i.e when the user try to copy or compare the iterator of one type of storage with one of another type of storage). I didn't decide on any special semantics for that, but later there'll be probably some assertions for debug builds.

Some methods are still missing, such as container sizes, but this is the basic for my storing with handles system for game components. I'm also implementing an ArrayStoringFactory class, that inherits from StoringFactory, but I don't think it's code has anything interesting right now.

Let's see what happens in the next 15 days. After finishing up theses classes and adding any new needed method to the base classes, I'll make some unit tests and then the Transform component. Maybe I'll have some thoughts to write in this blog again.

All code in this post has been pretty-printed by hilite.me

Nenhum comentário:

Postar um comentário