Imagine if we could devise classes whose interface can be turned on or off at specific source code locations.
This could be used to build interfaces that are only available inside a transactional scope.
Attempting to use the interface outside the transactional scope would result in a compiler error.
Prologue
In Almost Affine we explored how to use compile-time counters to create
affine functions that can only be invoked once.
Assume the existence of a compile-time counter.
An affine function can be created by constraining the call operator on the counter value.
We are going to extend this technique to use the counter across several member functions.
Staged Type
Consider the case of two-step initialization, where the constructor does the first part of the initialization and
an init() function completes the initialization.
We would like to get a compiler error if we use the other member functions before the initialization has been completed.
We would also like a compiler error if we attempt to invoke the init() function twice.
The trick is that the init() function increments the compile-time counter just like the affine function,
and the call() function is constrained by the current counter value.
Obscurable Type
The staged type is an example of an obscurable type, where parts of the interface is enabled or disabled depending
on the use.
Let us create a class with functions to enable and disable the interface.
Both functions increments a compile-time counter, and the interface is only enabled when the counter is odd.
The set of enabled and disabled functions will alternate as the compile-time counter increases.
Counter
enable()
disable()
call()
0
✅
❌
❌
1
❌
✅
✅
2
✅
❌
❌
3
❌
✅
✅
…
The constraint used for call() can also be applied to other member functions we may add.
The enable and disable functions have been constrained by the compile-time counter to disallow nested use.
If nesting is required, then two compile-time counters are needed. One counter keeps track of
enable() calls and the other counter tracks disable() calls.
The call() function is disabled when these two counters are equal.
The enable and disable functions performs no run-time operations, but they could…
Transaction Lock
We can demonstrate the above-mentioned technique with a concrete example.
Let us create a lockable type that prevents us from invoking a function unless the mutex is locked.
The lock and associated unlock functions define a transactional scope.
The lockable type also has an invoke() function that only is invocable inside the transactional scope.
The obscurable types do not check whether the transactional scope is open when the object is destroyed.
It would be useful if we could check the transaction counter in the destructor, but to obtain the counter
we would have to turn the destructor into a template function which is not possible.
Caveat
The same severe caveats from Almost Affine also applies to the above.