Unsigned splitter (unsigned types to bytes)
Table of Contents
Some facts
Recently i've been working on embedded system without any system. To be honest its been quite fun, though somewhat challenging and maybe that is part of the fun. I heard that using c++ on embedded system with the small memory isn't really approriate. Though I will beg the difference, using c++20 with all its new cool features are quite good. I.e spans, string_view and concepts. Bare with me , im just scratching the surface when it comes to all these new concepts. But slowly im getting a hang on it, and while exploring I found it more and more useful. I was planning to do some investigation on using Rust on embedded, but to be fair it takes a while to get use to rust, and especially on embedded system.Anyhow, this is the first article of some of the things I found neat and useful during my development. Lately i've been reading Functional programmming in c++ , which is an excellent book (though I wish it was using c++20) and that kind of reflects the outcome of this article. Anyhow,Lets just dig right into it.
The problem
The problem that I had was that I wanted to split a uint16_t into bytes.
\begin{equation} f(\text{0xffaa}) \rightarrow [B_2\text{: 0xff}, B_1\text{ :0xaa}] \end{equation}So basically splitting up an unsinged number. This could be done in a real simple manner for example
#include <cstdint> int main() { constexpr uint32_t val=0xffaaccdd; constexpr uint8_t b1 = val & 0xff; constexpr uint8_t b2 = (val & 0xff'00) >> 8; constexpr uint8_t b3 = (val & 0xff'00'00) >> 16; constexpr uint8_t b4 = (val & 0xff'00'00'00) >> 24; std::cout << std::hex << static_cast<int>(b1) << ","<< static_cast<int>(b2) << \ "," << static_cast<int>(b3) << "," << static_cast<int>(b4) << "\n"; //std::cout << std::hex << static_cast<int>(b3) << ","<< static_cast<int>(b4) << "\n"; return 0; }
dd | cc | aa | ff |
But this becomes more cumbersome when it comes to larger sized unsigned. For example uint64_t. And if we look at the above code, we can se a pattern, lets start with the Rol function we call it R.
\begin{equation} R(i) = 8 \times (i-1) \end{equation}Where i is the index number of the byte we want. So for example:
This resembles the pattern for rolling to the left in the above code (>> R(i)). The next part is Masking, again we need the index. But this time the index decides how many moves we want to move the masking.
\begin{equation} R(1) = 8*(1-1) = 0 \\ R(2) = 8*(2-1) = 8 \\ R(3) = 8*(3-1) = 16 \\ R(3) = 8*(4-1) = 24 \\ \end{equation} \begin{equation} \text{Magic} = 0xff \\ M(i) = \text{Magic} << (8\times(i-1)) \\ M(i) = \text{Magic} << R(i)) \end{equation}In other words we want to move the Magic number \(n\times8\) times to the left. And now we can se that n is actually the same number as R(i)!
Which means that we get a equation looking like
\begin{equation} F(v,i) = ( v \& M(R(i))) >> R(i) \end{equation}Lets do simple table to see this happen using the 0xffaaccdd value above
$3 = '(format "%x" (lsh 255 $2));N
i | R(i) | M(i) | F(0xffaa…,i) | |
---|---|---|---|---|
1 | 0 | 255 | 221 | 221 |
2 | 8 | 65280 | 52224 | 204 |
3 | 16 | 16711680 | 11141120 | 170 |
4 | 24 | 4278190080 | 4278190080 | 255 |
So we proved it works.
Cpp implementation
To implement this in c++ we need to consider the types. In a pure mathematical sense we don't actually care about how the values fit into memory, but in c++ we have to.
I.e the masking, moving the 0xff N times to the left where N is 100 will make the memory overflow . In elisp this works , but in c++ this would go over the limit of any known integral type.
(ash 255 100)
323250903058198497381659317370880
That is a massivly big number, its actually \(2^{100} \times 2^8 = 2^{108}\), and as far as I know there is no such integral types in c++. The biggest built in integral type (as far as I know) is \(2^{64} = 18446744073709551616\) which is quite a bit smaller.
Lets define the functions as we have done mathematically before.
namespace { template<typename T> constexpr T magic = 0xff; constexpr auto R = [](std::unsigned_integral auto i) { return 8 * (i - 1); }; constexpr auto M = [](std::unsigned_integral auto x) { return magic<decltype(x)> << x; }; constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { return static_cast<uint8_t>((V & M(R(i))) >> R(i)); }; } // anonymous namespace int main() { constexpr uint32_t val=0xffaaccdd; std::cout << std::hex << static_cast<int>(F(val,1U)) << \ "," << static_cast<int>(F(val,2U)) << "," << static_cast<int>(F(val,3U)) << "," << static_cast<int>(F(val,4U)) << "\n"; return 0; }
dd | cc | aa | ff |
This seems to work reasonably well. Lets check it out for a 64-bit value.
namespace { template<typename T> constexpr T magic = 0xff; constexpr auto R = [](std::unsigned_integral auto i) { return 8 * (i - 1); }; constexpr auto M = [](std::unsigned_integral auto x) { return magic<decltype(x)> << x; }; constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { return static_cast<uint8_t>((V & M(R(i))) >> R(i)); }; } // anonymous namespace int main() { constexpr uint64_t val=0x08'07'06'05'04'03'02'01; for(unsigned i = 0; i < 9 ; ++i) { std::cout << std::hex << static_cast<int>(F(val,i)) << ","; } return 0; }
0 | 1 | 2 | 3 | 4 | 0 | 0 | 0 | 0 |
That result is definetly not what we expected and wanted. So what happened? The problem is mentioned before is that the type of the returning M function, The code above is actually using the same type as x in the M function. But since x is injected as argument from R which gets its value from i which is the index number and defined to be unsigned int (32 bit). The magic number will become 32-bit, which is to small.
Lets dive into just the M function, by arithmentically bit shift the magic value 64 times, we get:
namespace { template<typename T> constexpr T magic = 0xff; constexpr auto M = [](std::unsigned_integral auto x) { return magic<decltype(x)> << x; }; } // int main() { for (unsigned i=1; i < 64; ++i) { std::cout << M(i) << ( i % 8 == 0 ? "\n" : ","); } return 0; }
510 | 1020 | 2040 | 4080 | 8160 | 16320 | 32640 | 65280 |
130560 | 261120 | 522240 | 1044480 | 2088960 | 4177920 | 8355840 | 16711680 |
33423360 | 66846720 | 133693440 | 267386880 | 534773760 | 1069547520 | 2139095040 | 4278190080 |
4261412864 | 4227858432 | 4160749568 | 4026531840 | 3758096384 | 3221225472 | 2147483648 | 255 |
510 | 1020 | 2040 | 4080 | 8160 | 16320 | 32640 | 65280 |
130560 | 261120 | 522240 | 1044480 | 2088960 | 4177920 | 8355840 | 16711680 |
33423360 | 66846720 | 133693440 | 267386880 | 534773760 | 1069547520 | 2139095040 | 4278190080 |
4261412864 | 4227858432 | 4160749568 | 4026531840 | 3758096384 | 3221225472 | 2147483648 |
As we can note this wraps around when the value goes beyond 2147483648 which is the same as \(2^{31}\). So the type we are using for the magic number is not appropriate in case the size of i 32 bit. Can we do something about it? Of course we could use a i type which is a uint64_t but that is just hiding the problem.
Lets set the type of the magic number to uint64_t (64-bit) the it should work, lets check it out.
namespace { template<typename T> constexpr T magic = 0xff; constexpr auto M = [](std::unsigned_integral auto x) { return magic<decltype(x)> << x; }; } // int main() { for (uint64_t i=1; i < 64; ++i) { std::cout << M(i) << ( i % 8 == 0 ? "\n" : ","); } return 0; }
510 | 1020 | 2040 | 4080 | 8160 | 16320 | 32640 | 65280 |
130560 | 261120 | 522240 | 1044480 | 2088960 | 4177920 | 8355840 | 16711680 |
33423360 | 66846720 | 133693440 | 267386880 | 534773760 | 1069547520 | 2139095040 | 4278190080 |
8556380160 | 17112760320 | 34225520640 | 68451041280 | 136902082560 | 273804165120 | 547608330240 | 1095216660480 |
2190433320960 | 4380866641920 | 8761733283840 | 17523466567680 | 35046933135360 | 70093866270720 | 140187732541440 | 280375465082880 |
560750930165760 | 1121501860331520 | 2243003720663040 | 4486007441326080 | 8972014882652160 | 17944029765304320 | 35888059530608640 | 71776119061217280 |
143552238122434560 | 287104476244869120 | 574208952489738240 | 1148417904979476480 | 2296835809958952960 | 4593671619917905920 | 9187343239835811840 | 18374686479671623680 |
18302628885633695744 | 18158513697557839872 | 17870283321406128128 | 17293822569102704640 | 16140901064495857664 | 13835058055282163712 | 9223372036854775808 |
Yes! I hypothesis worked. Should we be happy with this? Nah, I would like to have the magic to be of the same type as the val, that way we always use the same type, and it would not using more memory then necessary. But to do that we need somehow to specify what type magic should have. Fortunatly this can be done with templates.
namespace { template<typename T> constexpr static T magic = 0xff; constexpr auto R = [](std::unsigned_integral auto i) { return 8 * (i - 1); }; template<std::unsigned_integral T> auto M(T x) { return magic<T> << x; } constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { return static_cast<uint8_t>((V & M(static_cast<decltype(V)>(R(i)))) >> R(i)); }; } // int main() { constexpr uint64_t val=0xff'ee'dd'bb'cc'aa'02'01; for (unsigned i=1; i < 9; ++i) { std::cout << std::hex << static_cast<int>(F(val,i)) << ','; } return 0; }
1 | 2 | aa | cc | bb | dd | ee | ff |
voila! It worked, and now we are using the same type for val and magic. I hope this rambling makes some sense. This looks somewhat messy, but let me explain the F function. Lets break it down abit.
1: 2: constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { 3: auto r = R(i); 4: auto m = M(static_cast<decltype(V)>(r)); 5: 6: return static_cast<uint8_t>((V & m) >> r); 7: }; 8:
In this scenario the value V is an uint64_t and i is unsigned variable.
- Line 2
- Call function R witha argument i this will return an uint32_t (variable r is now uint32_t)
- Line 3
- Call function M but convert the r-variable to an uint64_t , the call to M will be with a uint64_t value, which means it cannot overflow. That means m will be an uint64_t type.
- Line 5
- mask the value (uint64_t) with m (uint64_t), right shift r times and the output will be an uint8_t value.
This works pretty well, but lets extend abit further with some helper functionality.
Helper Function
Sometimes you just want to transform the value into something that is indexed. For example an array or a tuple. Here is an example implementation:
#include <string> #include <concepts> #include <tuple> template <typename T> constexpr T magic = 0xff; constexpr auto f_rev = [](auto sz, auto i) { return sz -1U - i; }; constexpr auto R = [](std::unsigned_integral auto i) { return 8 * (i - 1); }; template <std::unsigned_integral T> auto M(T x) { return magic<T> << x; } constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { return static_cast<uint8_t>((V & M(static_cast<decltype(V)>(R(i)))) >> R(i)); }; template <std::unsigned_integral T, std::size_t... I> constexpr auto fold_it(T val, std::index_sequence<I...>) { return std::make_tuple(F(val, I + 1)...); } template <std::unsigned_integral T> constexpr auto unsigned_to_byte_tuple(T val) { constexpr size_t val_size = sizeof(val); return fold_it(val, std::make_index_sequence<val_size>{}); } int main(int argc, char *argv[]) { uint64_t val = 0xff'ee'dd'cc'bb'aa'99'88; auto tpl = unsigned_to_byte_tuple(val); std::cout << std::hex << static_cast<int>(std::get<0>(tpl)) << "," <<static_cast<int>(std::get<1>(tpl)) << "," <<static_cast<int>(std::get<2>(tpl)) << "," <<static_cast<int>(std::get<3>(tpl)) << "," <<static_cast<int>(std::get<4>(tpl)) << "," <<static_cast<int>(std::get<5>(tpl)) << "," <<static_cast<int>(std::get<6>(tpl)) << "," <<static_cast<int>(std::get<7>(tpl)) << '\n'; return 0; }
88 | 99 | aa | bb | cc | dd | ee | ff |
Integer seqeuence
The core of the above function is the integer_sequence or index_sequence which is a specialization of the former. With integer_sequence we can at combile time create a sequence of numbers. The only problem is that the sequence is only created from 0…N , but sometimes its necessary to mutate the sequence, and of course it should be possible to mutate the sequence during compile time.
Funcuntional mutator
The idea here is to have a external function that mutates the sequence for each value, for example reverse it.
#include <cstdint> #include <cstring> constexpr auto f_static = []([[maybe_unused]]auto sz,[[maybe_unused]] auto i) { return 0; }; constexpr auto f_ls = [](auto sz, auto i) { return 1<<i; }; constexpr auto f_rev = [](auto sz, auto i) { return sz -1U - i; }; template <typename Fn,typename T, T... Ints> constexpr decltype(auto) alter_seq( Fn fn, std::index_sequence<Ints...> ) { return std::integer_sequence<size_t,fn(sizeof...(Ints),Ints)...>{}; } // Taken from https://en.cppreference.com/w/cpp/utility/integer_sequence template<typename T, T ...Ints> void print_sequence(std::integer_sequence<T, Ints...> int_seq) { static_cast<void>(int_seq); ((std::cout << Ints << ','), ...); std::cout << '\n'; } int main(int argc, char *argv[]) { print_sequence(std::make_integer_sequence<std::size_t,10>{}); print_sequence(alter_seq(f_static,std::make_integer_sequence<std::size_t,10>{})); print_sequence(alter_seq(f_ls,std::make_integer_sequence<std::size_t,10>{})); print_sequence(alter_seq(f_rev,std::make_integer_sequence<std::size_t,10>{})); // Using reversing it twice should make it back to the original sequence. print_sequence(alter_seq(f_rev,alter_seq(f_rev,std::make_integer_sequence<std::size_t,10>{}))); return 0; }
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | |
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
This is more generic. Now we can provide the sequence function that we need. The only thing that bothers me is the way we are chaining functions which gets a bit messy. So can we do anything about it? Lets try
The problem now is that we have quite an verbose calling sequence
(i.e
alter_seq(f_static,std::make_integer_sequence<std::size_t,10>{})
),
to make it easier we could add a operator for example =|.
Lets try that with the next example.
1: #include <cstdint> 2: #include <cstring> 3: 4: 5: 6: constexpr auto f_add_2 = []([[maybe_unused]]auto sz, auto i) 7: { 8: return i+2; 9: }; 10: 11: 12: constexpr auto f_rev = [](auto sz, auto i) 13: { 14: return sz -1U - i; 15: }; 16: 17: template <typename Fn,typename T, T... Ints> 18: constexpr decltype(auto) alter_seq( Fn fn, std::index_sequence<Ints...> ) 19: { 20: return std::integer_sequence<size_t,fn(sizeof...(Ints),Ints)...>{}; 21: } 22: // Taken from https://en.cppreference.com/w/cpp/utility/integer_sequence 23: template<typename T, T ...Ints> 24: void print_sequence(std::integer_sequence<T, Ints...> int_seq) 25: { 26: static_cast<void>(int_seq); 27: ((std::cout << Ints << ','), ...); 28: std::cout << '\n'; 29: } 30: 31: 32: template<typename Fn,typename T, T...Ints> 33: requires std::invocable<Fn,size_t,unsigned> 34: constexpr decltype(auto) operator|=(std::index_sequence<Ints...> ix, Fn&& fn) 35: { 36: return alter_seq(fn,ix); 37: } 38: 39: 40: 41: 42: int main(int argc, char *argv[]) 43: { 44: 45: auto seq = (std::make_integer_sequence<std::size_t,10>{} |= f_rev) |= f_add_2; 46: print_sequence(seq); 47: 48: return 0; 49: }
11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
Line 45 chains the output from function f_rev
as input to
f_add_2
this means that we have a sequence of 0,2,3,4….9
each of the value is sent to the f_rev function that will
reverse the sequence. The output will be another sequence, this
sequence is then chained to f_add_2. And finally the output
will be whatever f_add_2 returns. This of course is nice the
only snag is the parenthesis to perform one caluclation to be
input to the next, in other words we always need to create a
integer_sequence before calling the next function. But can we do
better? We could add another operator that takes 2 functions and
then add them toghether. e.g \(f(sz,i) = x \rightarrow g(sz,x) =
y\) this should be the same as doing \(f(sz,g(sz,i)) = h(sz,i)\) to
merge two function together to one we need another function that is
invocable \(h(sz,i)\). So how do we chain two functions, by creating
another lambda function of course.
1: #include <cstdint> 2: #include <cstring> 3: 4: template<typename Fn, typename Gn> 5: requires std::invocable<Fn,size_t, unsigned> && std::invocable<Gn,size_t, unsigned> 6: constexpr decltype(auto) operator+(Fn&& fn, Gn&& gn) 7: { 8: return [fn,gn](size_t sz, unsigned i) 9: { 10: 11: return fn(sz,gn(sz,i)); 12: }; 13: } 14: 15: 16: int main(int argc, char *argv[]) 17: { 18: auto fn = [](size_t sz, unsigned i) 19: { 20: std::cout << "Exec Fn("<< i <<")\n"; 21: 22: return i+2; 23: }; 24: 25: auto gn = [](size_t sz, unsigned i) 26: { 27: std::cout << "Exec Gn("<< i <<")\n"; 28: return i+3; 29: }; 30: 31: auto dn = [](size_t sz, unsigned i) 32: { 33: std::cout << "Exec Dn("<< i << ")\n"; 34: return i-2; 35: }; 36: 37: auto h = fn + gn + dn; 38: 39: std::cout << h(10,1) << '\n'; 40: 41: return 0; 42: } 43:
Exec | Dn(1) |
Exec | Gn(4294967295) |
Exec | Fn(2) |
4 |
When we ran the above program we got this as output, that is not exactly what we wanted. The order of evaluation is from right to left. If we break it down we can actually see the problem more clearly.
\begin{equation} h0(sz,i_1) = fn(sz,gn(sz,i_1))\\ h(sz,i) = h0(sz,dn(sz,i)) \rightarrow h(sz,i) = fn(sz,gn(sz,dn(sz,i))) \end{equation}So it kind of make sense, but that is not how we want it.. What we want to reverse it. so for example (removing sz for the time being)
\begin{equation} h(i)=dn(gn(fn(i))) \end{equation}1: #include <cstdint> 2: #include <cstring> 3: 4: template<typename Fn, typename Gn> 5: requires std::invocable<Fn,size_t, unsigned> && std::invocable<Gn,size_t, unsigned> 6: constexpr decltype(auto) operator+(Fn&& fn, Gn&& gn) 7: { 8: return [fn,gn](size_t sz, unsigned i) 9: { 10: return gn(sz,fn(sz,i)); 11: }; 12: } 13: 14: 15: int main(int argc, char *argv[]) 16: { 17: auto fn = [](size_t sz, unsigned i) 18: { 19: auto tmp = i+2; 20: std::cout << "Exec Fn("<< i << ") " << tmp << "\n"; 21: 22: return tmp; 23: }; 24: 25: auto gn = [](size_t sz, unsigned i) 26: { 27: 28: auto tmp = i+3; 29: std::cout << "Exec Gn("<< i << ") " << tmp << "\n"; 30: return tmp; 31: }; 32: 33: auto dn = [](size_t sz, unsigned i) 34: { 35: 36: auto tmp = i-2; 37: std::cout << "Exec Dn("<< i << ") " << tmp << "\n"; 38: return tmp; 39: 40: }; 41: 42: auto h = fn + gn + dn; 43: 44: std::cout << h(10,1) << '\n'; 45: 46: return 0; 47: } 48:
Exec | Fn(1) | 3 |
Exec | Gn(3) | 6 |
Exec | Dn(6) | 4 |
4 |
That is much more like it. Now lets use that on the integer_sequence construct we used before.
Operators on sequence.
1: #include <cstdint> 2: #include <cstring> 3: 4: 5: constexpr auto f_add_2 = []([[maybe_unused]]auto sz, auto i) 6: { 7: return i+2; 8: }; 9: 10: 11: constexpr auto f_rev = [](auto sz, auto i) 12: { 13: return sz -1U - i; 14: }; 15: 16: constexpr auto f_remove_1 = [](auto sz, auto i) 17: { 18: 19: return i-1; 20: }; 21: 22: 23: 24: 25: template <typename Fn,typename T, T... Ints> 26: constexpr decltype(auto) alter_seq( Fn fn, std::index_sequence<Ints...> ) 27: { 28: return std::integer_sequence<size_t,fn(sizeof...(Ints),Ints)...>{}; 29: } 30: // Taken from https://en.cppreference.com/w/cpp/utility/integer_sequence 31: template<typename T, T ...Ints> 32: void print_sequence(std::integer_sequence<T, Ints...> int_seq) 33: { 34: static_cast<void>(int_seq); 35: ((std::cout << Ints << ','), ...); 36: std::cout << '\n'; 37: } 38: 39: 40: template<typename Fn,typename T, T...Ints> 41: requires std::invocable<Fn,size_t,unsigned> 42: constexpr decltype(auto) operator|=(std::index_sequence<Ints...> ix, Fn&& fn) 43: { 44: return alter_seq(fn,ix); 45: } 46: 47: template<typename Fn, typename Gn> 48: requires std::invocable<Fn,size_t, unsigned> && std::invocable<Gn,size_t, unsigned> 49: constexpr decltype(auto) operator|=(Fn&& fn, Gn&& gn) 50: { 51: return [fn,gn](size_t sz, unsigned i) 52: { 53: 54: return gn(sz,fn(sz,i)); 55: }; 56: } 57: 58: 59: 60: int main(int argc, char *argv[]) 61: { 62: 63: constexpr auto seq = std::make_integer_sequence<std::size_t,10>{} |= f_rev |= f_add_2 |= f_remove_1; 64: print_sequence(seq); 65: 66: return 0; 67: }
10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
It all works out fine, so lets add go back to the original problem.
Lets put this all toghether
Lets now put toghether what we learnt, and apply the ideas of integer seqeuence function
1: #include <array> 2: #include <iostream> 3: #include <string> 4: #include <tuple> 5: 6: namespace 7: { 8: 9: 10: template <typename T> 11: constexpr T magic = 0xff; 12: 13: constexpr auto bits_used = 8; 14: 15: constexpr auto f_rev = [](auto sz, auto i) { return sz - 1U - i; }; 16: 17: template <typename Fn, typename T, T... Ints> 18: constexpr auto alter_seq(Fn fn, std::index_sequence<Ints...>) 19: { 20: return std::integer_sequence<size_t, fn(sizeof...(Ints), Ints)...>{}; 21: } 22: 23: constexpr auto R = [](std::unsigned_integral auto i) { return bits_used * (i - 1); }; 24: 25: template <std::unsigned_integral T> 26: constexpr auto M(T x) 27: { 28: return magic<T> << x; 29: } 30: 31: constexpr auto F = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { 32: return static_cast<uint8_t>((V & M(static_cast<decltype(V)>(R(i)))) >> R(i)); 33: }; 34: 35: 36: constexpr auto F_xor = [](std::unsigned_integral auto V, std::unsigned_integral auto i) -> uint8_t { 37: return static_cast<uint8_t>((V & M(static_cast<decltype(V)>(R(i)))) >> R(i)) ^ 0xaa; 38: }; 39: 40: //------------------------------------------------------------ 41: template <typename Fn, typename T, T... Ints> 42: requires std::invocable<Fn, size_t, unsigned> 43: constexpr decltype(auto) operator|=(std::index_sequence<Ints...> ix, Fn&& fn) 44: { 45: return alter_seq(fn, ix); 46: } 47: 48: template <typename Fn, typename Gn> 49: requires std::invocable<Fn, size_t, unsigned> && std::invocable<Gn, size_t, unsigned> 50: constexpr decltype(auto) operator|=(Fn&& fn, Gn&& gn) 51: { 52: return [fn, gn](size_t sz, unsigned i) { return gn(sz, fn(sz, i)); }; 53: } 54: //------------------------------------------------------------ 55: 56: template <std::unsigned_integral T, std::invocable<size_t,unsigned> Fn,std::size_t... I> 57: constexpr decltype(auto) for_each_val(T val, std::index_sequence<I...> , Fn F) 58: { 59: return std::make_tuple(F(val, I + 1)...); 60: } 61: 62: template <std::unsigned_integral T> 63: constexpr auto split_to_byte_tuple(T val) 64: { 65: constexpr size_t val_size = sizeof(val); 66: 67: auto seq = std::make_integer_sequence<std::size_t, val_size>{}; 68: return for_each_val(val, seq,F); 69: } 70: 71: template <std::unsigned_integral T> 72: constexpr auto split_to_byte_reverse_xor(T val) 73: { 74: constexpr size_t val_size = sizeof(val); 75: 76: auto seq = std::make_integer_sequence<std::size_t, val_size>{} |=f_rev; 77: return for_each_val(val, seq, F_xor); 78: } 79: 80: } // namespace 81: int main() 82: { 83: { 84: constexpr uint16_t val = 0b00000001'00000010; 85: auto tpl = split_to_byte_tuple( val); 86: 87: std::cout << "----------"<< std::tuple_size<decltype(tpl)>{} << "----------"<<"\n"; 88: 89: std::cout << "byte_0 " << std::hex << static_cast<int>(std::get<0>(tpl)) << "\n"; 90: std::cout << "byte_1 " << std::hex << static_cast<int>(std::get<1>(tpl)) << "\n"; 91: 92: } 93: 94: { 95: constexpr uint32_t val = 0b00000001'00000010'00000011'00000100; 96: constexpr auto tpl = split_to_byte_tuple( val); 97: std::cout << "----------"<< std::tuple_size<decltype(tpl)>{} << "----------"<<"\n"; 98: 99: std::cout << "byte_0 " << std::hex << static_cast<int>(std::get<0>(tpl)) << "\n"; 100: std::cout << "byte_1 " << std::hex << static_cast<int>(std::get<1>(tpl)) << "\n"; 101: std::cout << "byte_2 " << std::hex << static_cast<int>(std::get<2>(tpl)) << "\n"; 102: std::cout << "byte_3 " << std::hex << static_cast<int>(std::get<3>(tpl)) << "\n"; 103: } 104: 105: 106: { 107: constexpr uint64_t val = 108: 0b11111111'00000010'00000011'00000100'00000101'00000110'00000111'00001000; 109: constexpr auto tpl = split_to_byte_tuple( val); 110: std::cout << "----------"<< std::tuple_size<decltype(tpl)>{} << "----------"<<"\n"; 111: 112: std::cout << "byte_0 " << std::hex << static_cast<int>(std::get<0>(tpl)) << "\n"; 113: std::cout << "byte_1 " << std::hex << static_cast<int>(std::get<1>(tpl)) << "\n"; 114: std::cout << "byte_2 " << std::hex << static_cast<int>(std::get<2>(tpl)) << "\n"; 115: std::cout << "byte_3 " << std::hex << static_cast<int>(std::get<3>(tpl)) << "\n"; 116: std::cout << "byte_4 " << std::hex << static_cast<int>(std::get<4>(tpl)) << "\n"; 117: std::cout << "byte_5 " << std::hex << static_cast<int>(std::get<5>(tpl)) << "\n"; 118: std::cout << "byte_6 " << std::hex << static_cast<int>(std::get<6>(tpl)) << "\n"; 119: std::cout << "byte_7 " << std::hex << static_cast<int>(std::get<7>(tpl)) << "\n"; 120: } 121: 122: { 123: constexpr uint64_t val = 124: 0b11111111'00000010'00000011'00000100'00000101'00000110'00000111'00001000; 125: constexpr auto tpl = split_to_byte_reverse_xor(val); 126: std::cout << "------rev-"<< std::tuple_size<decltype(tpl)>{} << "-xor------"<<"\n"; 127: 128: std::cout << "byte_0 " << std::hex << static_cast<int>(std::get<0>(tpl)) << "\n"; 129: std::cout << "byte_1 " << std::hex << static_cast<int>(std::get<1>(tpl)) << "\n"; 130: std::cout << "byte_2 " << std::hex << static_cast<int>(std::get<2>(tpl)) << "\n"; 131: std::cout << "byte_3 " << std::hex << static_cast<int>(std::get<3>(tpl)) << "\n"; 132: std::cout << "byte_4 " << std::hex << static_cast<int>(std::get<4>(tpl)) << "\n"; 133: std::cout << "byte_5 " << std::hex << static_cast<int>(std::get<5>(tpl)) << "\n"; 134: std::cout << "byte_6 " << std::hex << static_cast<int>(std::get<6>(tpl)) << "\n"; 135: std::cout << "byte_7 " << std::hex << static_cast<int>(std::get<7>(tpl)) << "\n"; 136: } 137: 138: 139: 140: return 0; 141: } 142:
-----–—2---------- | |
byte_0 | 2 |
byte_1 | 1 |
-----–—4---------- | |
byte_0 | 4 |
byte_1 | 3 |
byte_2 | 2 |
byte_3 | 1 |
-----–—8---------- | |
byte_0 | 8 |
byte_1 | 7 |
byte_2 | 6 |
byte_3 | 5 |
byte_4 | 4 |
byte_5 | 3 |
byte_6 | 2 |
byte_7 | ff |
-–—rev-8-xor------ | |
byte_0 | 55 |
byte_1 | a8 |
byte_2 | a9 |
byte_3 | ae |
byte_4 | af |
byte_5 | ac |
byte_6 | ad |
byte_7 | a2 |
Restoring
This is exactly the opposite to splitting up, we now have a tuple with N bytes. And now we want to restore it to its original type T? Somehow we need to provide a policy how to restore it to the orignal value.
Lets see how this is done with just the byte-tuple the original way with a 32 bit.
#include <cstring> #include <tuple> int main() { uint32_t val{}; auto tpl = std::make_tuple(0xff,0xee,0xdd,0xcc); val = std::get<3>(tpl) << 8*0; val |= std::get<2>(tpl) << 8*1; val |= std::get<1>(tpl) << 8*2; val |= std::get<0>(tpl) << 8*3; std::cout << val << "," << static_cast<unsigned>(0xff'ee'dd'cc)<< '\n'; return 0; }
4293844428 | 4293844428 |
Again we see the a pattern here, that shouln't be a big surprise. So how do we break it down. Again we see the sequence i
\begin{equation} R(sz,i)=8\times(sz-i) \\ V(i,tpl)=get < i >(tpl) \ll R(sz,i) \end{equation}
The problem that we will head straight into is that std::get
will
need the i variable to be constructed at compile time, so we will
not be able to use it as a parameter to the funtcion, instead we need
to provide it as a template argument. What we want is for each of the
values: transform the value based on the index i and merge it
together with the previous value. There are a couple of different way
of doing this. One easy way is to use std::apply . What std::apply
does is to use the std::tuple and divided it up into parameters to a
a specified function. lets do a small example:
#include <tuple> #include <string> #include <optional> int main(int argc, char *argv[]) { auto f = [](unsigned i, std::string str, double d) { std::cout << "i " << i << "\nstr " << str << "\nd "<< d << "\n"; return std::optional<unsigned>(i); }; constexpr auto tpl = std::make_tuple(100, "Hello", 3.4); auto opt = std::apply(f,tpl); return 0; }
i | 100 |
str | Hello |
d | 3.4 |
However, this doesn't solve the problem since we don't know how many
items consists in the tuple. Forutnatly we can use the variadic
argument to lambda together with fold expression. The good thing with
a fold expression is also the fact that we can use |
operator
directly. Lets demonstrate this with an similar example:
1: #include <tuple> 2: #include <string> 3: #include <optional> 4: 5: int main(int argc, char *argv[]) 6: { 7: auto f = [](const auto&...args) 8: { 9: size_t i{}; 10: auto val = ((args << i++)|...); 11: return std::optional<unsigned>(val); 12: }; 13: 14: constexpr auto tpl = std::make_tuple(0x01,0x02,0x04); 15: 16: auto opt = std::apply(f,tpl); 17: 18: (opt ? std::cout << "Got " << *opt << "\n" : std::cout << "Error!" << "\n"); 19: 20: 21: return 0; 22: } 23:
Got 21 |
So how did we get that? Let disect line 10 line by line as it expands the fold:
\begin{equation} args \ll 0 = 1 << 0 = 1 \\ args \ll 1 = 2 << 1 = 4 \\ args \ll 2 = 4 << 2 = 16 \\ (1 | (4 | 16)) = 21 \end{equation}This of course could restore the byte sequence to it's original type. E.g
1: #include <tuple> 2: 3: 4: template< typename T, size_t sz = sizeof(T)> 5: constexpr T R( unsigned i) 6: { 7: return 8* static_cast<T>(sz - i); 8: } 9: 10: 11: template<typename T> 12: constexpr T V(size_t i,auto tplVal) 13: { 14: return static_cast<T>(tplVal)<< R<T>(i); 15: } 16: 17: 18: 19: 20: template<std::unsigned_integral U_Type, typename...Ts> 21: constexpr auto F(std::tuple<Ts...> tpl) 22: { 23: 24: auto f = [](Ts const&... tplArgs) 25: { 26: size_t i{1}; 27: 28: auto val = (V<U_Type>(i++,tplArgs)|...); 29: return val; 30: }; 31: return std::apply(f,tpl); 32: } 33: 34: int main() 35: { 36: constexpr auto tpl = std::make_tuple(0xff,0xee,0xdd,0xcc); 37: { 38: 39: constexpr auto f = F<uint32_t>(tpl); 40: std::cout << f << " " << uint32_t{0xffeeddcc} << '\n'; 41: 42: } 43: 44: { 45: constexpr auto tpl = std::make_tuple(0x01,0x02,0x03,0x04); 46: constexpr auto f = F<uint32_t>(tpl); 47: std::cout << f << " " << uint32_t{0x01020304} << '\n'; 48: } 49: 50: { 51: constexpr auto tpl = std::make_tuple(0x01,0x02); 52: constexpr auto f = F<uint16_t>(tpl); 53: std::cout << f << " " << uint16_t{0x0102} << '\n'; 54: } 55: 56: { 57: constexpr auto tpl = std::make_tuple(0x01,0x02,0x03,0x04,0x01,0x02,0x03,0x04); 58: constexpr auto f = F<uint64_t>(tpl); 59: std::cout << f << " " <<uint64_t{0x01'02'03'04'01'02'03'04}<<'\n'; 60: } 61: 62: return 0; 63: }
4293844428 | 4293844428 |
16909060 | 16909060 |
258 | 258 |
72623859723010820 | 72623859723010820 |
For now
Yep, I will stop here…. Maybe I'll continue some other day..