Tracked Types

1.) Create a source data class

The first step to using hist is creating your data class (note that this documentation may use "class" and "struct" interchangably). At present hist supports:

  • Fundamental types (e.g. int, float, char, std::uint8_t)

  • C arrays

  • std::array’s

  • std::vector’s

  • std::optional’s

  • std::string’s *

  • Types which support both operator= and serialization/deserialization via reinterpret_cast to bytes **

  • Any struct or class containing the above

* Note that std::string along with other containers could be greatly improved by recieving their own set of specialized ops like std::vector has, and may not at this time be appropriate for large, heavily mutated strings (e.g. a notepad application)
** Future support for hist and serialization customization could improve this

2.) Reflect!

Unless you’re living in the post-C++26 future - you need to include reflect.h and use a reflection macro (note that macroless reflection is insufficent, due to the need for adaptive structures) - this is easiest to write inside the class, but you may also reflect from outside the class.

struct Npc_data
{
    std::string name = "";
    int hitpoints = 0;

    REFLECT(Npc_data, name, hitpoints) // inside is preferred
};
//REFLECT_PRIVATE(Npc_data, name, hitpoints) // outside is fine if needed

3.) Create your tracked class

You can use nf::make_tracked, if your use case is trivially simple, e.g.

auto jj = nf::make_tracked(Npc_data{.name = "jj", .hitpoints = 120});
Run

In most cases you’ll want to create your own tracked class by extending nf::tracked; you need to provide your source data class and your tracked class as template arguments, and you need a constructor initializing tracked with this. e.g.

struct Npc : nf::tracked<Npc_data, Npc>
{
    Npc() : tracked(this) {}
};

4.) Initialize your data

You have a few options here…​

  • You can do "nothing" and lean on the source data classes default member initializers and/or default constructor (optionally followed by a call to record_init). Example

  • You can perform an "untracked" initialization using the init_data<false> method - you supply an instance of your source data struct to be moved into the tracked storage; no history will be generated for the initialization event. Example

  • You can perform a "tracked" initialization using the init_data<true> method - you supply an instance of your source data struct to be moved into the tracked storage, and this will be recorded as the first action in hist. Example

Note that init_data can only be called when history is empty, you can use .clear_history or you can replace your entire object without clearing history using tracked()→assign(..)

Additionally, note that your initialization event does not have to be tracked for all cases, e.g. undo-redos will be fine without it, but it’s recommended for others, such as making replays resilient to code changes.

5.) Try reading from your data!

struct Npc_data
{
    std::string name = "jj";
    int hitpoints = 120;

    REFLECT(Npc_data, name, hitpoints)
};

struct Npc : nf::tracked<Npc_data, Npc>
{
    Npc() : tracked(this) {}
};

int main()
{
    Npc npc {};
    assert(npc.read.hitpoints == 120);
    assert(npc->hitpoints == 120);
    std::cout << Json::out(*npc) << std::endl;
}
Run

The member variable read is a const & to your stored source data class (Npc_data) that you can use, alternatively you can use pointer semantics; you can also leverage RareCpp’s JSON library if you wish as your struct is already reflected.

Note that you can edit your data with the tracked()→ syntax, such that → is reading and ()→ is modifying, but for reasons we’ll get into in the next section, creating an explicit action is usually preferrable.