These are phantom type parameters, that is, parameters of a parameterised type that are used not for their representation, but to separate different “spaces” of types with the same representation.
And speaking of spaces, that’s a useful application of phantom types:
template<typename Space>
struct Point { double x, y; };
struct WorldSpace;
struct ScreenSpace;
// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) { … }
As you’ve seen, though, there are some difficulties with unit types. One thing you can do is decompose units into a vector of integer exponents on the fundamental components:
template<typename T, int Meters, int Seconds>
struct Unit {
Unit(const T& value) : value(value) {}
T value;
};
template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
return a.value / b.value;
}
Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);
// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;
Here we’re using phantom values to tag runtime values with compile-time information about the exponents on the units involved. This scales better than making separate structures for velocities, distances, and so on, and might be enough to cover your use case.
The selling points of the Pimpl pattern are:
- total encapsulation: there are no (private) data members mentioned in the header file of the interface object.
- stability: until you break the public interface (which in C++ includes private members), you'll never have to recompile code that depends on the interface object. This makes the Pimpl a great pattern for libraries that don't want their users to recompile all code on every internal change.
- polymorphism and dependency injection: the implementation or behaviour of the interface object can be easily swapped out at runtime, without requiring dependent code to be recompiled. Great if you need to mock something for an unit test.
To this effect, the classic Pimpl consists of three parts:
An interface for the implementation object, which must be public, and use virtual methods for the interface:
class IFrobnicateImpl
{
public:
virtual int frobnicate(int) const = 0;
};
This interface is required to be stable.
An interface object that proxies to the private implementation. It does not have to use virtual methods. The only allowed member is a pointer to the implementation:
class Frobnicate
{
std::unique_ptr<IFrobnicateImpl> _impl;
public:
explicit Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl = nullptr);
int frobnicate(int x) const { return _impl->frobnicate(x); }
};
...
Frobnicate::Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl /* = nullptr */)
: _impl(std::move(impl))
{
if (!_impl)
_impl = std::make_unique<DefaultImplementation>();
}
The header file of this class must be stable.
At least one implementation
The Pimpl then buys us a great deal of stability for a library class, at the cost of one heap allocation and additional virtual dispatch.
How does your solution measure up?
- It does away with encapsulation. Since your members are protected, any subclass can mess with them.
- It does away with interface stability. Whenever you change your data members – and that change is just one refactoring away – you'll have to recompile all dependent code.
- It does away with the virtual dispatch layer, preventing easy swapping of the implementation.
So for every objective of the Pimpl pattern, you fail to fulfil this objective. It is therefore not reasonable to call your pattern a variation of the Pimpl, it is much more an ordinary class. Actually, it's worse than an ordinary class because your member variables are private. And because of that cast which is a glaring point of fragility.
Note that the Pimpl pattern is not always optimal – there's a tradeoff between stability and polymorphism on the one hand, and memory compactness on the other. It is semantically impossible for a language to have both (without JIT compilation). So if you're micro-optimizing for memory compactness, clearly the Pimpl is not a suitable solution for your use case. You'll also probably stop using half the standard library, since these awful string and vector classes involve dynamic memory allocations ;-)
Best Answer
The compiler operates under the as-if rule that allows any and all code transformations that don't change the observable behavior of the program.
[C++14: 1.5/8]
[C11 5.1.2.3.6 Program execution] has a similar wording:
A delay is not considered an observable behavior and the first example can be "optimized" to an empty program.
Note that, to allow compiler transformations such as removal of empty loops (even when termination cannot be proven), C++14 standards says:
[C++14: 1.10/24]
(see also Does C/C++ offer any guarantee on minimal execution time?)
The second example is harder for the compiler because it is usually unable to analyze the code of an external library to determine whether it does / doesn't perform I/O or volatile access. However statically-linked third-party library code may be subject to link-time optimization so a multi-file organization isn't an insurmountable barrier.
The third example doesn't introduce anything new:
hardware_timer_isr
from being optimized outbut the
time
variable isn't manually designated as a variable that can be changed by interrupt handlers so the instructions:haven't an observable behavior and can be optimized out (see Why does the compiler not optimize away interrupt code? for further details).
If you need a delay you can use
std::sleep_for
orstd::sleep_until
.FURTHER NOTES
The reason of the note ("this is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven") is there's no way to detect infinite loops universally and the inability to prove termination hampers compilers which could otherwise make useful transformations (there is a good example in N1528: Why undefined behavior for infinite loops?).
For the C++ language the situation is described in:
For the C language C11 provides an exception for controlling expressions that are constant expressions.