I wanted to do a couple of sanity tests for a pair of convenience functions that split a 64-bit integer in two 32-bit integers, or do the reverse. The intent is that you don't do the bit shifts and logic ops all over again with the potential of a typo somewhere. The sanity tests were supposed to make 100% sure that the pair of functions, although pretty trivial, indeed works as intended.
Nothing fancy, really... so as the first thing I added this:
static constexpr auto joinsplit(uint64_t h) noexcept { auto [a,b] = split(h); return join(a,b); }
static_assert(joinsplit(0x1234) == 0x1234);
... which works perfectly well, but is less "exhaustive" than I'd like. Of course I can follow up with another 5 or 6 tests with different patterns, copy-paste to the rescue. But seriously... wouldn't it be nice to have the compiler check a dozen or so values, within a pretty little function? No copy-paste? Now that would be cool.
With a recursive variadic template, this can be done (and it's what I'm using in lack of something better), but it's in my opinion needlessly ugly.
Given the power of constexpr
functions and range-based for
, wouldn't it be cool to have something nice and readable like:
constexpr bool test()
{
for(constexpr auto value : {1,2,3}) // other numbers of course
{
constexpr auto [a,b] = split(value);
static_assert(value == join(a,b));
}
return true; // never used
}
static_assert(test()); // invoke test
A big plus of this solution would be that in addtion to being much more readable, it would be obvious from the failing static_assert
not just that the test failed in general, but also the exact value for which it failed.
This, however, doesn't work for two reasons:
value
as constexpr
because, as stated by the compiler: "The value of __for_begin
is not usable in a constant expression". The reason for that is also explained by the compiler: "note: __for_begin
was not declared constexpr
". Fair enough, that is a reason, silly as it may be.constexpr
(which is promptly followed by a non-constexpr condition for static_assert
error).In both cases, I wonder if there is truly a hindrance to allowing these being constexpr
. I understand why it doesn't work (see above!), but the interesting question is why is it like that?
I acknowledge that declaring value
as constexpr
is a lie to begin with since its value obviously is not constant (it's different in each iteration). On the other hand, any value that it ever takes is from a compiletime constant set of values, yet without the constexpr
keyword the compiler refuses to treat it as such, i.e. the result of split
is non-constexpr
and not usable with static_assert
although it really is, by all means.
OK, well... I'm probably really asking too much if I want to declare something that has a changing value as constant. Even though from some point of view, if it is constant, in each iteration's scope. Somehow... is the language missing a concept here?
I acknowledge that range-based for
is, like lambdas, really just a hack that mostly works, and mostly works invisibly, not a true language feature -- the mention of __for_begin
is a dead giveaway on its implementation. I also acknowledge that it's generally tricky (forbidding) to allow the counter in a normal for
loop being constexpr
, not only because it's not constant, but because you can in principle have any kind of expressions in there, and it truly cannot be easily told in advance what values in general will be generated (not with reasonable effort during compiletime, anyway).
On the other hand, given an exact finite sequence of literals (which is as compiletime-constant as it can get), the compiler should be able to do a number of iterations, each iteration of the loop with a different, compiletime-constant value (unroll the loop if you will). Somehow, in a readable (non-recursive-template) manner, such thing should be possible?
Am I asking too much there?
I acknowledge that a decomposition declaration is not an altogether "trivial" thing. It might for example require calling get
on a tuple, which is a class template (that could in principle be anything). But, whatever, get
happens to be constexpr
(so that's no excuse), and also in my concrete example, an anonymous temporary of an anonymous struct with two members is returned, so public direct member binding (to a constexpr
struct
) is used.
Ironically, the compiler even does exactly the right thing in the first example, too (and with recursive templates as well). So apparently, it's quite possible. Only just, for some reason, not in the second example.
Again, am I asking too much here?
The likely correct answer will be "The standard doesn't provide that".
Apart from that, are there any true, technical reasons why this cannot, could not, or should not work? Is that an oversight, an implementation deficiency, or intentionally forbidden?
I can't answer you theoretical questions (" is the language missing a concept here?", " such thing should be possible? Am I asking too much there?", "there any true, technical reasons why this cannot, could not, or should not work? Is that an oversight, an implementation deficiency, or intentionally forbidden?") but, from the practical point of view...
With a recursive variadic template, this can be done (and it's what I'm using in lack of something better), but it's in my opinion needlessly ugly.
I think that variadic templates is the right way and (you tagged C++17), using folding, there is no reason to recursivize it.
By example
template <uint64_t ... Is>
static constexpr void test () noexcept
{ static_assert( ((joinsplit(Is) == Is) && ...) ); }
The following is a full compiling example
#include <utility>
#include <cstdint>
static constexpr std::pair<uint32_t, uint32_t> split (uint64_t h) noexcept
{ return { h >> 32 , h }; }
static constexpr uint64_t join (uint32_t h1, uint32_t h2) noexcept
{ return (uint64_t{h1} << 32) | h2; }
static constexpr auto joinsplit (uint64_t h) noexcept
{ auto [a,b] = split(h); return join(a, b); }
template <uint64_t ... Is>
static constexpr void test () noexcept
{ static_assert( ((joinsplit(Is) == Is) && ...) ); }
int main()
{
test<1, 2, 3>();
}
-- EDIT -- Bonus answer
Folding (C++17) is great but never underestimate the power of comma operator.
You can obtain the same result (well... quite same) in C++14 with an helper function and the initialization of an unused array
template <uint64_t I>
static constexpr void test_helper () noexcept
{ static_assert( joinsplit(I) == I, "!" ); }
template <uint64_t ... Is>
static constexpr void test () noexcept
{
using unused = int[];
(void)unused { 0, (test_helper<Is>(), 0)... };
}
Obviously after a little change in joinsplit()
to make it C++14 compliant
static constexpr auto joinsplit (uint64_t h) noexcept
{ auto p = split(h); return join(p.first, p.second); }