Say I wanted to write a generic class that maintains an integer that always stays between two values. Something like this:
template<int Lower, int Upper>
class MyInt {
private:
int m_int;
public:
// Constructors, operators...
};
The class invariant is that Lower <= m_int <= Upper
.
Of course MyInt should have all the usual operations that integers tend to have, like assignment and arithmetic operators. MyInt would throw if an operation were to leave it in a state that breaks its invariant. In many cases, however, this should be compile-time detectable. Consider this example code:
int foo = 500;
constexpr int const bar = 500;
MyInt<0,100> a = 15; // OK
MyInt<0,100> b = foo; // Throws at runtime
MyInt<0,100> c = 500; // Compile error?
MyInt<0,100> d = bar; // Compile error?
MyInt<0,100> f = std::integral_constant<int, 500>; // Compile error
For std::integral_constant
, writing the appropriate constructor is straight-forward. But is it possible to compile-time-detect that a
is within range and c
and d
aren't? The assigned values are either literals known at compile time or constexpr constants.
I have tried SFINAE-ing around and whatnot, but I could not find a way to go from value-semantics to template arguments, even for these cases where (I claim) the values are clearly compile-time constants.
Is it true for C++17 that the language does not provide the tools required to implement this? If yes, is this going to change with the upcoming C++20? In case what I'm looking for is impossible, what are the reasons for this? My interest is entirely educational, so I would be interested if I'm missing something (like literals not actually being compile-time constants or something).
Note: I am aware that case f
can be made less ugly by introducing a custom literal suffix and requiring the user to type something like this:
MyInt<0,100> g = 15_constint; // OK
MyInt<0,100> h = 500_constint; // Compile error
I am also aware of the possibility to provide an interface like this:
MyInt<0,100> i;
i.assign<500>(); // Compile error
However both these workarounds come with a certain typing overhead and are not necessarily idiomatic C++.
In case what I'm looking for is impossible, what are the reasons for this?
Basically it comes down to the fact that the C++ function model does not permit a function to recognize a distinction between a constexpr
parameter and a runtime parameter. If a function takes an int
, then it takes an int
. That function's behavior cannot be different based on whether that int
is supplied by a constant expression (like a literal) or a runtime value.
And this is broadly true even for constexpr
functions. You can't have a constexpr
function which can do compile-time checks on its parameters. The evaluation can be done at compile-time, but doing foo(500);
must ultimately behave the same as doing auto b = 500; foo(b);
.
This is why the workarounds all involve using tricks to put the constant expression in a template argument, where such recognition is possible. Of course, a template argument can never be a runtime value, so that creates other issues (like having to have multiple functions). And being a template argument is a generally painful thing to work with.
Essentially, what you want requires that a function can declare that a parameter is constexpr
(along with overloading rules to allow you to have a non-constexpr
version). No such proposal has been voted into C++20. The best that's been done is P1045, which hasn't even been discussed by the committee yet, and has a bunch of holes that need to be filled in. So I wouldn't hold my breath.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With