UP | HOME

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..

Date: 2022-07-03 Sun 00:00

Author: Calle Olsen

Created: 2022-09-18 Sun 17:52

Validate