Corrade::Utility::Tweakable class

Tweakable constants.

Provides a mechanism to immediately reflect changes to literals in source code to a running application. Useful when tweaking positions, colors, physics factors and other constants as it reduces the usual

  1. make a change to a literal,
  2. save,
  3. compile & link,
  4. restart the application,
  5. observe the difference

workflow to just

  1. make a change to a (marked) literal,
  2. save,
  3. observe the difference.

Works best combined with a traditional hot-reload approach (such as loading a dynamic module via PluginManager), which can take care of source code changes that tweakable constants alone can't.

Basic usage

Common usage is to first define a shorter alias to the CORRADE_TWEAKABLE() macro in the source file where you want to use tweakable values. It's possible to define any alias and you can also use the CORRADE_TWEAKABLE name directly. Here we'll use a single underscore:

#define _ CORRADE_TWEAKABLE

After that, enable it using enable() (it's disabled by default). In all code that gets executed from that point onwards, all literals wrapped with the macro invocation — in this case _() — will get recognized by it. To reflect source changes in the app, periodically call update(), for best responsiveness ideally in each event loop iteration. The following example implements a 2D movement, with the gravity and linear speed being tweakable:

Utility::Tweakable tweakable;

void init() {
    tweakable.enable();

    // …
}

void mainLoop() {
    fallVelocity += _(9.81f)*dt;
    position.x += _(2.2f)*dt;
    position.y += fallVelocity*dt;

    // …

    tweakable.update();
}

Then, if you build & run the code, the update() function will reparse the input source file each time it's saved, providing updated values for literals marked with _() in the running application. All operation is logged into the console, with the above example you'd see something like the following if the source gets modified while the app is running:

Utility::Tweakable::update(): looking for updated _() macros in main.cpp
Utility::Tweakable::update(): updating _(-9.81f) in main.cpp:14
Utility::Tweakable::update(): updating _(2.3f) in main.cpp:15

The implementation ensures the runtime-modified values are interpreted exactly the same way as if the code would be compiled directly from the modified source file. If that's not possible for whatever reason, update() will emit an error and won't update anything — at which point you can fall back to a traditional hot reload approach, for example.

Using scopes

Not all code is running in every iteration of an event loop — and it's not desirable to put it there just to be able to use tweakable constants. To fix that, there's the scope() function. It takes a single-parameter function (or a lambda) and runs the contents as if the code was placed directly in the containing block. But for every tweakable constant inside, it remembers its surrounding scope lambda. Then, during update(), whenever one of these constants is changed, the corresponding scope lambda gets called again (with the same parameter). So for example this way you can execute a part of a constructor again in a response to a change of one of its init parameters:

explicit App() {
    // …

    tweakable.scope([](App& app) {
        app.dt = _(0.01666667f); // 60 FPS
        app.fallVelocity = _(0.0f);
        app.position = {_(5.0f), _(150.0f)};
    }, *this);
}

void mainLoop() {
    fallVelocity += _(9.81f)*dt;
    // …

Note that lambdas passed to scope() may be called from update() in a random order and multiple times, so make sure to keep all referenced data in scope and handle the reentrancy properly.

Disabling tweakable values

Even though the implementation is designed for $ \mathcal{O}(1) $ lookup of tweakable values (a hashmap lookup for the file and direct indexing for given value), you may still want to disable it entirely. There are two possibilities:

  • You can remove all overhead at compile time by defining your alias to an empty value, thus all tweakable literals become just surrounded by parentheses:

    #define _

    If you're using CORRADE_TWEAKABLE directly, define it to an empty value before including Corrade/Utility/Tweakable.h. The header will detect that and not redefine it.

    #define CORRADE_TWEAKABLE
    #include "Corrade/Utility/Tweakable.h"
  • Or you can disable it at runtime by not calling enable(). That'll still make the values go through a function call, but they are simply passed through without any additional hashmap lookup.

In both cases the scope() function is practically just executing the passed lambda and (in case the tweakable is enabled) also saving a pointer to it, so its performance overhead is negligible. A non-enabled instance of Tweakable is internally just one pointer with no allocations involved.

Limitations

This is not magic, so it comes with a few limitations:

  • It's only possible to affect values of literals annotated by the CORRADE_TWEAKABLE() macro or its alias, this utility is not able to pick up changes to code around. Neither it's able to parse any arithmetic expressions done inside the tweakable macros — but unary + or - for numeric types is supported.
  • Adding a new constant on a line already containing other constants might result in a false success, mixing up the constant values.
  • The CORRADE_TWEAKABLE() macro depends on the __COUNTER__ preprocessor variable in order to distinguish multiple tweakable constants on the same line. This implies that using tweakable constants in header files (or *.cpp files that get #include d in other files) will break the counter and confuse the update() function.
  • An alias to the CORRADE_TWEAKABLE() has to be defined at most once in the whole file and all tweakable constants in that file have to use a single alias. On the other hand it's possible to have different aliases in different files.
  • Annotated literals are required to keep their type during edits — so it's not possible to change e.g. _(42.0f) to _(21.0), because that'll change the type from float to double. While it usually generates at most a warning from the compiler, such change may break source code change detection in unexpected ways.
  • Tweakable variables inside code that's compiled-out by the preprocessor (such as various #ifdef s) will confuse the runtime parser, so avoid them entirely.
  • For simplicity of the implementation, comments are not allowed inside the tweakable macros, only whitespace.

At the moment, the implementation is not thread-safe.

How it works

For each literal annotated with CORRADE_TWEAKABLE() or its alias, the class remembers its file, line and index (in order to correctly handle multiple literals on a single line) when the code is first executed, together with a TweakableParser instance corresponding to type of the literal known at compile time. Affected source files are then monitored with FileWatcher for changes

Upon calling update(), modified files are parsed for occurrences of the defined macro and arguments of each macro call are parsed at runtime. If there is any change, TweakableState::Success is returned and the next time code with given annotated literal is executed (either by the caller or directly through one of the scopes), the class will supply the updated value instead. If no files are modified or if the modification didn't result in any literal update, TweakableState::NoChange is returned.

If parsing the updated literals fails (because of a syntax error or because the mark is not just a literal), the update() function returns TweakableState::Error and doesn't update anything, waiting for the user to fix the error. If there is some mismatch detected (such as the constant having a different type than before or appearing on a different line), TweakableState::Recompile is returned and you are encouraged to trigger the classical hot-reload approach (or restart a recompiled version of the app).

Extending for custom types

It's possible to extend the builtin support for custom user-defined C++11 literals by providing a specialization of the TweakableParser class. See its documentation for more information.

References

Original idea for the implementation was taken from the Tweakable Constants article by Joel Davis, thanks goes to Alexey Yurchenko (@alexesDev) for sharing this article.

Public static functions

static auto instance() -> Tweakable&
Current instance.

Constructors, destructors, conversion operators

Tweakable() explicit
Constructor.
Tweakable(const Tweakable&) deleted
Copying is not allowed.
Tweakable(Tweakable&&) deleted
Moving is not allowed.
~Tweakable()
Destructor.

Public functions

auto operator=(const Tweakable&) -> Tweakable& deleted
Copying is not allowed.
auto operator=(Tweakable&&) -> Tweakable& deleted
Moving is not allowed.
auto isEnabled() const -> bool
Whether tweakable constants are enabled.
void enable()
Enable tweakable constants.
void enable(Containers::StringView prefix, Containers::StringView replace)
Enable tweakable constants with a relocated file watch prefix.
auto update() -> TweakableState
Update the tweakable constant values.
template<class T>
void scope(void(*)(T&) lambda, T& userData)
Tweakable scope.
void scope(void(*)(void*) lambda, void* userData = nullptr)
Tweakable scope.

Function documentation

static Tweakable& Corrade::Utility::Tweakable::instance()

Current instance.

Expects that an instance exists.

Corrade::Utility::Tweakable::Tweakable() explicit

Constructor.

Makes a global instance available to the CORRADE_TWEAKABLE() macro. Expects no global instance exists yet. Tweakable constants are disabled by default, call enable() before any of them is used to enable them.

Corrade::Utility::Tweakable::~Tweakable()

Destructor.

Unregisters the global instance.

bool Corrade::Utility::Tweakable::isEnabled() const

Whether tweakable constants are enabled.

void Corrade::Utility::Tweakable::enable()

Enable tweakable constants.

Tweakable constants are disabled by default, meaning all annotated constants are just a pass-through for the compiled value and scope() just calls the passed lambda without doing anything else.

Be sure to call this function before any tweakable constant or scope() is used for consistent results. Calling the function again after the tweakable was already enabled will cause the instance to reset all previous internal state.

void Corrade::Utility::Tweakable::enable(Containers::StringView prefix, Containers::StringView replace)

Enable tweakable constants with a relocated file watch prefix.

The enable() function implicitly uses the information from preprocessor __FILE__ macros to locate the source files on disk. With some buildsystems the __FILE__ information is relative to the build directory and in other cases you may want to watch files in a directory different from the source tree. This function strips prefix from all file paths and prepends replace to them using Path::join().

It's possible to have either prefix or replace empty, having both empty is equivalent to calling the parameter-less enable().

Be sure to call this function before any tweakable constant or scope() is used for consistent results. Calling the function again after the tweakable was already enabled will cause the instance to reset all previous internal state.

TweakableState Corrade::Utility::Tweakable::update()

Update the tweakable constant values.

Parses all files that changed and updates tweakable values. For every value that was changed and was part of a scope() call, executes the corresponding scope lambda — but every lambda only once.

If the tweakable is not enabled, does nothing and returns TweakableState::NoChange.

template<class T>
void Corrade::Utility::Tweakable::scope(void(*)(T&) lambda, T& userData)

Tweakable scope.

Executes passed lambda directly and also on every change to tweakable variables inside the lambda. See Using scopes for an usage example.

If the tweakable is not enabled, only calls the lambda without doing anything else.

void Corrade::Utility::Tweakable::scope(void(*)(void*) lambda, void* userData = nullptr)

Tweakable scope.

Equivalent to the above, but for lambdas with a generic typeless parameter. Or when you don't need any parameter at all and so the lambda gets just nullptr.

Define documentation

#define CORRADE_TWEAKABLE(...)

Tweakable constant annotation.

See Corrade::Utility::Tweakable for more information. Expects that an instance of the class exists when this macro is used. If the tweakable is not enabled, simply passes the value through.