class
#include <Corrade/Utility/Tweakable.h>
Tweakable 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
- make a change to a literal,
- save,
- compile & link,
- restart the application,
- observe the difference
workflow to just
- make a change to a (marked) literal,
- save,
- 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_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 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 fromfloat
todouble
. 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_
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::
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::
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
Constructors, destructors, conversion operators
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
Corrade:: Utility:: Tweakable:: Tweakable() explicit
Constructor.
Makes a global instance available to the CORRADE_
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::
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::
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::