Convert last characters of std::array<char, 10> to int - c++

Given the following array: std::array<char, 10> stuff I'd like to convert the last 4 characters to the corresponding int32 value.
I tried to chain OR operations on the last items but doesn't seem to be the right way:
int a = int(stuff[6] | stuff[7] | stuff[8] | stuff[9])
Is there an elegant way to solve this?

What you tried to do has elegance that comes across in not needing an endianness check in order to work properly. What you missed was some shifting to indicate significance in the final value:
int a = stuff[6] << 24 | stuff[7] << 16 | stuff[8] << 8 | stuff[9];
This alone does not care about endianness because from the language's perspective, it is based on values rather than bytes. You determine which values are most significant.
That said, this also assumes an 8-bit byte and at least 4-byte int. If you want elegance of use, you can get it with a safe and general abstraction:
#include <array>
#include <climits>
#include <cstddef>
namespace detail {
// Could be replaced by an inline lambda-template in C++20.
template<typename T, std::size_t N, std::size_t... Is>
constexpr T pack_into_impl(const std::array<std::byte, N>& bytes, std::index_sequence<Is...>) {
// Build final value from right to left to make the math more clear
// and to use the least significant bytes available when N < sizeof(T).
// e.g., bytes[3] << 0 | bytes[2] << 8 | bytes[1] << 16 | bytes[0] << 24
return ((static_cast<int>(bytes[N-Is-1]) << (CHAR_BIT * Is)) | ...);
}
}
// Takes bytes to pack from most significant to least significant.
// N.B. this is not a production-ready doc comment for this function.
template<typename T, std::size_t N>
constexpr T pack_into(std::array<std::byte, N> bytes) {
static_assert(sizeof(T) >= N, "Destination type is too small for this many bytes");
return detail::pack_into_impl<T>(bytes, std::make_index_sequence<N>{});
}
// Convenience overload.
template<typename T, typename... Bytes>
constexpr T pack_into(Bytes... bytes) {
// Check that each Bytes type can be static_cast to std::byte.
// Maybe check that values fit within a byte.
return pack_into<T>(std::array{static_cast<std::byte>(bytes)...});
}
int main() {
static_assert(pack_into<int>(0x12, 0x34, 0x56, 0x78) == 0x12345678);
static_assert(pack_into<int>(0x01, 0x02) == 0x0102);
// pack_into<int>(0x01, 0x02, 0x03, 0x04, 0x05); // static_assert
}
Some of this can be cleaned up in C++20 by using concepts and a []<std::size_t... Is> lambda, but you get the idea. Naturally, you're also free to transform the API to make the size unknown at compile-time for convenience and live with a possible runtime check when too many bytes are given. It depends on your use case.

Believe it or not, even though this is C++, memcpy() is the recommended way to do this kind of thing:
int32_t a;
memcpy(&a, stuff.data() + 6, 4);
It avoids strict aliasing violations, and compilers will optimize the memcpy call away.
Be aware of endianess differences if the data you're loading was created on a different machine with a different CPU architecture.

Related

Standard compliant host to network endianess conversion

I am amazed at how many topics on StackOverflow deal with finding out the endianess of the system and converting endianess. I am even more amazed that there are hundreds of different answers to these two questions. All proposed solutions that I have seen so far are based on undefined behaviour, non-standard compiler extensions or OS-specific header files. In my opinion, this question is only a duplicate if an existing answer gives a standard-compliant, efficient (e.g., use x86-bswap), compile time-enabled solution.
Surely there must be a standard-compliant solution available that I am unable to find in the huge mess of old "hacky" ones. It is also somewhat strange that the standard library does not include such a function. Perhaps the attitude towards such issues is changing, since C++20 introduced a way to detect endianess into the standard (via std::endian), and C++23 will probably include std::byteswap, which flips endianess.
In any case, my questions are these:
Starting at what C++ standard is there a portable standard-compliant way of performing host to network byte order conversion?
I argue below that it's possible in C++20. Is my code correct and can it be improved?
Should such a pure-c++ solution be preferred to OS specific functions such as, e.g., POSIX-htonl? (I think yes)
I think I can give a C++23 solution that is OS-independent, efficient (no system call, uses x86-bswap) and portable to little-endian and big-endian systems (but not portable to mixed-endian systems):
// requires C++23. see https://gcc.godbolt.org/z/6or1sEvKn
#include <type_traits>
#include <utility>
#include <bit>
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return std::byteswap(i);
}
}
Since std::endian is available in C++20, one can give a C++20 solution for host_to_net by implementing byteswap manually. A solution is described here, quote:
// requires C++17
#include <climits>
#include <cstdint>
#include <type_traits>
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) {
return ((((i >> (N * CHAR_BIT)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * CHAR_BIT)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
The linked answer also provides a C++11 byteswap, but that one seems to be less efficient (not compiled to x86-bswap). I think there should be an efficient C++11 way of doing this, too (using either less template-nonsense or even more) but I don't care about older C++ and didn't really try.
Assuming I am correct, the remaining question is: can one can determine system endianess before C++20 at compile time in a standard-compliant and compiler-agnostic way? None of the answers here seem to do achieve this. They use reinterpret_cast (not compile time), OS-headers, union aliasing (which I believe is UB in C++), etc. Also, for some reason, they try to do it "at runtime" although a compiled executable will always run under the same endianess.)
One could do it outside of constexpr context and hope it's optimized away. On the other hand, one could use system-defined preprocessor definitions and account for all platforms, as seems to be the approach taken by Boost. Or maybe (although I would guess the other way is better?) use macros and pick platform-specific htnl-style functions from networking libraries(done, e.g., here (GitHub))?
compile time-enabled solution.
Consider whether this is useful requirement in the first place. The program isn't going to be communicating with another system at compile time. What is the case where you would need to use the serialised integer in a compile time constant context?
Starting at what C++ standard is there a portable standard-compliant way of performing host to network byte order conversion?
It's possible to write such function in standard C++ since C++98. That said, later standards bring tasty template goodies that make this nicer.
There isn't such function in the standard library as of the latest standard.
Should such a pure-c++ solution be preferred to OS specific functions such as, e.g., POSIX-htonl? (I think yes)
Advantage of POSIX is that it's less important to write tests to make sure that it works correctly.
Advantage of pure C++ function is that you don't need platform specific alternatives to those that don't conform to POSIX.
Also, the POSIX htonX are only for 16 bit and 32 bit integers. You could instead use htobeXX functions instead that are in some *BSD and in Linux (glibc).
Here is what I have been using since C+17. Some notes beforehand:
Since endianness conversion is always1 for purposes of serialisation, I write the result directly into a buffer. When converting to host endianness, I read from a buffer.
I don't use CHAR_BIT because network doesn't know my byte size anyway. Network byte is an octet, and if your CPU is different, then these functions won't work. Correct handling of non-octet byte is possible but unnecessary work unless you need to support network communication on such system. Adding an assert might be a good idea.
I prefer to call it big endian rather than "network" endian. There's a chance that a reader isn't aware of the convention that de-facto endianness of network is big.
Instead of checking "if native endianness is X, do Y else do Z", I prefer to write a function that works with all native endianness. This can be done with bit shifts.
Yeah, it's constexpr. Not because it needs to be, but just because it can be. I haven't been able to produce an example where dropping constexpr would produce worse code.
// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;
template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
unsigned char* buf,
T t,
[[maybe_unused]] std::index_sequence<I...>) noexcept
{
using U = std::make_unsigned_t<promote_t<T>>;
constexpr U lastI = sizeof(T) - 1u;
constexpr U bits = 8u;
U u = t;
( (buf[I] = u >> ((lastI - I) * bits)), ... );
}
template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept
{
using Indices = std::make_index_sequence<sizeof(T)>;
return host_to_big_impl<T>(buf, t, Indices{});
}
1 In all use cases I've encountered. Conversions from integer to integer can be implemented by delegating these if you have such case, although they cannot be constexpr due to need for reinterpret_cast.
I made a benchmark comparing my C++ solution from the question and the solution by eeroika from the accepted answer.
Looking at this is a complete waste of time, but now that I did it, I though I might as well share it. The result is that (in the specific not-quite-realistic usecase I look at) they seem to be equivalent in terms of performance. This is despite my solution being compiled to use x86-bswap, while the solution by eeroika does it by just using mov.
The performance seems to differ a lot (!!) when using different compilers and the main thing I learned from these benchmarks is, again, that I'm just wasting my time...
// benchmark to compare two C++20-stand-alone host-to-big-endian endianess conversion.]
// Run at quick-bench.com! This is not a complete program. (https://quick-bench.com/q/2qnr4xYKemKLZupsicVFV_09rEk)
// To run locally, include Google benchmark header and a main method as required by the benchmarking library.
// Adapted from https://stackoverflow.com/a/71004000/9988487
#include <type_traits>
#include <utility>
#include <cstddef>
#include <cstdint>
#include <climits>
#include <type_traits>
#include <utility>
#include <bit>
#include <random>
/////////////////////////////// Solution 1 ////////////////////////////////
template <typename T> struct scalar_t { T t{}; /* no begin/end */ };
static_assert(not std::ranges::range< scalar_t<int> >);
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) noexcept {
constexpr auto bits_per_byte = 8u;
static_assert(bits_per_byte == CHAR_BIT);
return ((((i >> (N * bits_per_byte)) & (T)(unsigned char)(-1)) <<
((sizeof(T) - 1 - N) * bits_per_byte)) | ...);
}; // ^~~~~ fold expression
template<class T, class U = typename std::make_unsigned<T>::type>
constexpr U bswap(T i) noexcept {
return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
constexpr inline auto host_to_net(std::integral auto i) {
static_assert(std::endian::native == std::endian::big || std::endian::native == std::endian::little);
if constexpr (std::endian::native == std::endian::big) {
return i;
} else {
return bswap(i); // replace by `std::byteswap` once it's available!
}
}
/////////////////////////////// Solution 2 ////////////////////////////////
// helper to promote an integer type
template <class T>
using promote_t = std::decay_t<decltype(+std::declval<T>())>;
template <class T, std::size_t... I>
constexpr void
host_to_big_impl(
unsigned char* buf,
T t,
[[maybe_unused]] std::index_sequence<I...>) noexcept {
using U = std::make_unsigned_t<promote_t<T>>;
constexpr U lastI = sizeof(T) - 1u;
constexpr U bits = 8u;
U u = t;
( (buf[I] = u >> ((lastI - I) * bits)), ... );
}
template <class T, std::size_t... I>
constexpr void
host_to_big(unsigned char* buf, T t) noexcept {
using Indices = std::make_index_sequence<sizeof(T)>;
return host_to_big_impl<T>(buf, t, Indices{});
}
//////////////////////// Benchmarks ////////////////////////////////////
template<std::integral T>
std::vector<T> get_random_vector(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. Don't use in production code!
std::mt19937_64 rng{seed};
std::uniform_int_distribution<T> distribution(
std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
std::vector<T> result(length);
for (auto && val : result) {
val = distribution(rng);
}
return result;
}
template<>
std::vector<bool> get_random_vector<bool>(std::size_t length, unsigned int seed) {
// NOTE: IT IS VERY SLOW TO RECREATE RNG EVERY TIME. ONLY USE FOR TESTING!
std::mt19937_64 rng{seed};
std::bernoulli_distribution distribution{0.5};
std::vector<bool> vec(length);
for (auto && val : vec) {
val = distribution(rng);
}
return vec;
}
constexpr std::size_t n_ints{1000};
static void solution1(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
host_to_big(buffer.data() + sizeof(int)*i, intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution1);
static void solution2(benchmark::State& state) {
std::vector<int> intvec = get_random_vector<int>(n_ints, 0);
std::vector<std::uint8_t> buffer(sizeof(int)*intvec.size());
for (auto _ : state) {
for (std::size_t i{}; i < intvec.size(); ++i) {
buffer[sizeof(int)*i] = host_to_net(intvec[i]);
}
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
BENCHMARK(solution2);

How can I convert ANY user defined type to an std::bitset?

What I want to achieve is a means of converting any arbitrarily sized and formatted type to an std::bitset. Like this:
#include<bitset>
#include<bit>
#include<cstdlib>
#include<cstdint>
#include<array>
#include<iostream>
template<typename T, std::size_t SIZE = (sizeof(T) * CHAR_BIT)>
std::bitset<SIZE> as_bits(const T var) noexcept
{
if constexpr (SIZE < 32)//Size in bits
{
int32_t temp = 0;
std::memmove(&temp, &var, sizeof(T));
std::bitset<SIZE> bits = var;
return bits;
}//End if
else
{
std::bitset<SIZE> bits = std::bit_cast<std::bitset<SIZE>, T>(var);
return bits;
}//End else
}//End of as_bits
Usage:
float x = 4.5f;
std::cout << x << " as bits: " << as_bits(x) << "\n";
#pragma pack(push)
struct Y
{
std::array<int32_t, 4> z;
float x;
int8_t y;
};
#pragma pack(pop)
Y y = { {1,2,3,4}, 3.5, 'a'};
std::cout << "struct as bits: " << as_bits(y) << "\n";
std::cout << "size of bitset: " << as_bits(y).size() << " bits long.\n";
Output:
4.5 as bits: 01000000100100000000000000000000
struct as bits: 000000000000000000000000011000010100000001100000000000000000000000000000000000000000000000000100000000000000000000000000000000110000000000000000000000000000001000000000000000000000000000000001
size of bitset: 192 bits long.
This works for correctly the float but the struct when converted outputs 192 bits when it should only be 168 bits in size. What's going on I've got #pragma pack?
How can I prevent padding? Should I even?
Is there a way to lockout padded types using concepts or type traits?
Is this undefined behavior?
Does endian-ness matter?
Is there a better way?
I'm using MSVC at the moment but a cross-platform implementation would be ideal.
On MSVC changing #pragma pack(push) to #pragma pack(push, 1)
results in the following error:
Error C2783 '_To std::bit_cast(const _From &) noexcept': could not deduce template argument for '__formal'
Does bit_cast require default padding and alignment?
Updated with a work around for types less than 32-bits in width.
What you want is not generally possible. Any user-defined type which is not trivially copyable is immediately off the table, because bit_cast only works on trivially copyable types.
Speaking of which, bitset itself is not required by the standard to be trivially copyable. I mean, there's pretty much no reason why an implementation of it wouldn't be, but there is nothing in the standard which requires implementers to make it trivially copyable. So while your code may function on some particular implementation (or likely all of them), there is no guarantee that you can do a bit_cast to a bitset at all.
As for why it can break with padding, this is likely because bit_cast also requires the two types to be the same size, and bitset<N> is not required to be N/8 bytes in size. Many implementations of bitset store the bits in arrays of 32-bit integer types. So a bitset<24> may still take up 4 bytes of storage. If you were given a 3-byte type, then you can't bit_cast them.
Odds are good that what you really want is an std::array<std::byte, sizeof(T)>. While this type is trivially copyable (so bit_cast can work on it), there actually isn't a requirement that the size of such an array is equal to the sizeof(T). It usually will be, but you can't guarantee it. The size will be implementation-dependent, so whether bit_casting from a trivially copyable T works will be implementation-dependent.
What's going on I've got #pragma pack?
#pragma pack can't break the rules of C++. And there are two rules of C++ that are important here:
sizeof(T) is also the number of bytes from one T to another T in an array of T.
Every T must be aligned to its alignof(T) alignment. Even if the T is an element in an array.
pack can't break these rules. Since your array and float are both undoubtedly aligned to 4 bytes, T must also be aligned to 4 bytes. And since a 21-byte array increment would not reach the 4 byte alignment needed by T, the size of T must be padded out to 24.
#pragma pack only plays around with packing within the rules of C++'s requirements.

C++11 way to check flags

The typical C-style approach:
#define LOG_ERRORS 1 // 2^0, bit 0
#define LOG_WARNINGS 2 // 2^1, bit 1
#define LOG_NOTICES 4 // 2^2, bit 2
#define LOG_INCOMING 8 // 2^3, bit 3
#define LOG_OUTGOING 16 // 2^4, bit 4
#define LOG_LOOPBACK 32 // and so on...
// Only 6 flags/bits used, so a char is fine
unsigned char flags;
// initialising the flags
flags = LOG_ERRORS;
//initialising to multiple values with OR (|)
flags = LOG_ERRORS | LOG_WARNINGS | LOG_INCOMING;
// sets to 1 + 2 + 8 i.e. bits 0, 1 and 3
// testing for a flag
// AND with the bitmask before testing with ==
if ((flags & LOG_WARNINGS) == LOG_WARNINGS)
...
// testing for multiple flags
// as above, OR the bitmasks
if ((flags & (LOG_INCOMING | LOG_OUTGOING))
== (LOG_INCOMING | LOG_OUTGOING))
...
Is there a better way in C++11, which keeps the old C style interface (LOG_INCOMING | LOG_OUTGOING)? i.e. How can I get rid of the "ugly" way to check which bits are set?
I have been looking at std::bitset but this latter only tests for positional queries (i.e. for example it can test if 3rd bit is set) and cannot test for something like this:
LOG_INCOMING | LOG_OUTGOING
I would replace your macros with static const ints (well, or an enum with explicit values), but other than that your implementation is just fine.
You should not replace good, solid, robust, clear, self-documenting, concise, reliable code with some new template monstrosity just because you can.
Your code is modern enough and this pattern is still very much in use.
I don't see anything wrong with the performance of the code that 0x/1y features will help. If it's already well tested, you probably want to avoid a re-write (especially if existing code depends on it).
If just want some ideas for how you could use features though, there are some different approaches you could take.
constexpr...
constexpr uint8_t bit(const uint8_t n) {
return 1 << n;
}
constexpr static const uint8_t LOG_ERRORS = bit(0);
constexpr static const uint8_t LOG_WARNINGS = bit(1);
if (flags & (LOG_ERROR | LOG_WARNINGS))
binary literals...
static const uint8_t LOG_ERRORS = 0b00000001;
static const uint8_t LOG_WARNINGS = 0b00000010;
if (flags & (LOG_ERRORS | LOG_WARNINGS))
variadic templates...
template<typename T, typename... Ts>
T bit_or(T t, Ts... ts) {
return t | bit_or(ts...);
}
template<typename T>
T bit_or(T t) {
return t;
}
template<typename T, typename... Ts>
bool any_set(T t, Ts... ts) {
return static_cast<bool>(t & (bit_or(ts...)));
}
constexpr uint8_t bit(const uint8_t n) {
return 1 << n;
}
constexpr static const uint8_t LOG_ERRORS = bit(0);
constexpr static const uint8_t LOG_WARNINGS = bit(1);
if (any_set(flags, LOG_ERRORS, LOG_WARNINGS))
My personal preference would be to avoid std::bitset, since it can't be directly evaluated in a boolean context. However, I might consider wrapping flags in a class, and using an enum class : uint8_t for the flags for type safety. The class would probably be something similar to Java's EnumSet. You could easily overload the bitwise operations (&, |, ^, ~, etc...) for it to preserve the C interface.
There is bitset, which, among other things, allows you to set an N bit to true; it also has some methods for conversions to unsigned and to string ( C++ string, not just a null terminated sequence of char C-style )
Other than that, I don't think that there is a more C++-ish way of doing this, but I'll probably keep an approach similar to what Lightness Races in Orbit just described, be conservative and don't add an overhead that you don't need .

size of a hex pattern in cpp

I have a hex pattern stored in a variable, how to do I know what is the size of the hex pattern
E.g. --
#define MY_PATTERN 0xFFFF
now I want to know the size of MY_PATTERN, to use somewhere in my code.
sizeof (MY_PATTERN)
this is giving me warning -- "integer conversion resulted in truncation".
How can I fix this ? What is the way I should write it ?
The pattern can increase or decrease in size so I can't hard code it.
Don't do it.
There's no such thing in C++ as a "hex pattern". What you actually use is an integer literal. See paragraph "The type of the literal". Thus, sizeof (0xffff) is equal to sizeof(int). And the bad thing is: the exact size may vary.
From the design point of view, I can't really think of a situation where such a solution is acceptable. You're not even deriving a type from a literal value, which would be a suspicious as well, but at least, a typesafe solution. Sizes of values are mostly used in operations working with memory buffers directly, like memcpy() or fwrite(). Sizes defined in such indirect ways lead to a very brittle binary interface and maintenance difficulties. What if you compile a program on both x86 and Motorola 68000 machines and want them to interoperate via a network protocol, or want to write some files on the first machine, and read them on another? sizeof(int) is 4 for the first and 2 for the second. It will break.
Instead, explicitly use the exactly sized types, like int8_t, uint32_t, etc. They're defined in the <cstdint> header.
This will solve your problem:
#define MY_PATTERN 0xFFFF
struct TypeInfo
{
template<typename T>
static size_t SizeOfType(T) { return sizeof(T); }
};
void main()
{
size_t size_of_type = TypeInfo::SizeOfType(MY_PATTERN);
}
as pointed out by Nighthawk441 you can just do:
sizeof(MY_PATTERN);
Just make sure to use a size_t wherever you are getting a warning and that should solve your problem.
You could explicitly typedef various types to hold hex numbers with restricted sizes such that:
typedef unsigned char one_byte_hex;
typedef unsigned short two_byte_hex;
typedef unsigned int four_byte_hex;
one_byte_hex pattern = 0xFF;
two_byte_hex bigger_pattern = 0xFFFF;
four_byte_hex big_pattern = 0xFFFFFFFF;
//sizeof(pattern) == 1
//sizeof(bigger_pattern) == 2
//sizeof(biggest_pattern) == 4
four_byte_hex new_pattern = static_cast<four_byte_hex>(pattern);
//sizeof(new_pattern) == 4
It would be easier to just treat all hex numbers as unsigned ints regardless of pattern used though.
Alternatively, you could put together a function which checks how many times it can shift the bits of the pattern until it's 0.
size_t sizeof_pattern(unsigned int pattern)
{
size_t bits = 0;
size_t bytes = 0;
unsigned int tmp = pattern;
while(tmp >> 1 != 0){
bits++;
tmp = tmp >> 1;
}
bytes = (bits + 1) / 8; //add 1 to bits to shift range from 0-31 to 1-32 so we can divide properly. 8 bits per byte.
if((bits + 1) % 8 != 0){
bytes++; //requires one more byte to store value since we have remaining bits.
}
return bytes;
}

Reading/Writing Nibbles (without bit fields) in C/C++

Is there an easy way to read/write a nibble in a byte without using bit fields?
I'll always need to read both nibbles, but will need to write each nibble individually.
Thanks!
Use masks :
char byte;
byte = (byte & 0xF0) | (nibble1 & 0xF); // write low quartet
byte = (byte & 0x0F) | ((nibble2 & 0xF) << 4); // write high quartet
You may want to put this inside macros.
The smallest unit you can work with is a single byte. If you want to manage the bits you should use bitwise operators.
Here's a modern answer that takes C++11 into account:
// Fixed-width integer types
#include <cstdint>
// Constexpr construction
constexpr uint8_t makeByte(uint8_t highNibble, uint8_t lowNibble)
{
return (((highNibble & 0xF) << 4) | ((lowNibble & 0xF) << 0));
}
// Constexpr high nibble extraction
constexpr uint8_t getHighNibble(uint8_t byte)
{
return ((byte >> 4) & 0xF);
}
// Constexpr low nibble extraction
constexpr uint8_t getLowNibble(uint8_t byte)
{
return ((byte >> 0) & 0xF);
}
Big benefits:
No nasty union trickery
No ugly macros
No boilerplate
Using standardised fixed-width types
contexpr functions
(I.e. can be used in compile-time calculations and template paramters.)
Just plain simple
(Before anyone asks, the >> 0 and << 0 are for primarily for visual balance, to demonstrate that the same concept is in use even in the exceptional case where no shift is actually needed. If your compiler doesn't optimise those away, complain to your compiler provider, not me.)
However, if your nibbles actually represent something important, e.g. a bitfield, then you might want to create a class/struct.
For example if you were programming a device that required a frame buffer with index 16-colour values, with 2 pixel values packed per byte, you might want to create something like this:
struct PixelPair
{
private:
uint8_t value;
public:
contexpr explicit PixelPair(uint8_t rawValue) :
value { rawValue }
{
}
constexpr PixelPair(uint8_t leftPixel, uint8_t rightPixel) :
value { makeByte(leftPixel, rightPixel) }
{
}
constexpr uint8_t getLeftPixel() const
{
return getHighNibble(this->value);
}
constexpr uint8_t getRightPixel() const
{
return getLowNibble(this->value);
}
constexpr uint8_t getRawValue() const
{
return this->value;
}
};
Note that this is essentially just a vanishingly thin wrapper around the above functions.
In this case it provides:
Type safety - No accidentally mixing up a plain old uint8_t and a specifically designated PixelPair. (See also: Bjarne Stroustrup's 2012 Keynote, where he discusses "Type-rich Programming".)
Improved readability - pixelPair.getLeftPixel() tells you exactly what the code is dealing with: the left-hand pixel of a pair of pixels.
Clear semantics - The code tells you what it is dealing with, not how it is dealing with it. pixelPair.getLeftPixel() tells you that the function is retrieving the left-hand pixel without specifying how, whereas getHighNibble(pixelByte) only tells you the how, i.e. that the high nibble of a pixel byte is being retrieved, it doesn't tell you what that nibble represents - perhaps the high nibble actually represents the right-hand pixel?
You could take this further and create a Pixel class too if you wanted even more type safety, and it could have relevant functions for dealing with the specific pixel format. This sort of code gets you thinking about what kind of data you are dealing with and the relationships between the data, rather than just thinking about the data as quantities of bits and bytes.
You could create yourself a pseudo union for convenience:
union ByteNibbles
{
ByteNibbles(BYTE hiNibble, BYTE loNibble)
{
data = loNibble;
data |= hiNibble << 4;
}
BYTE data;
};
Use it like this:
ByteNibbles byteNibbles(0xA, 0xB);
BYTE data = byteNibbles.data;