I have to deal with raw memory manipulation. For that I wrote a function which stores data one after another, and another function which reads this data and stores it into variables.
More precisely:
/**
* - `void * pack( dest, src1, ..., srcn )`
* `void * pack( void * dest, src1, ..., srcn )`
* Copies the bytes from values of src1, ..., srcn one after another to the address of dest
* The sum of sizeof(srci) must equal sizeof(dest), except, when dest is a void pointer in which no type checking is performed
* If an address of vari points to some memory of dest, the behaviour is undefined
* Returns a pointer one after the last written byte
*
* - `void unpack( src, dest1, ..., destn )`
* `void unpack( const void * src, dest1, ..., destn )`
* Similar to `pack`, but writes the content of src into the variables dest1, ..., destn
* Does not return anything
*
* ## Helpers
* - ´int64_t sum< ... >()`
* function which returns the sum of all template arguments, arguments must be integers
*/
I want to know
- Do I invoke undefined behavior anyplace, anywhere, anytime? When so,
- which tests I could write against it (i.e. which tests are likely to fail at the moment)?
- what else could I do to get the correct pointer arithmetic here?
- Is the interface of the functions sensible?
- Is the
__restrict__
qualifier of some use here? - Any other comments are also appreciated.
#ifndef CSTRINGUTILS_HPP
#define CSTRINGUTILS_HPP
#include <cassert>
#include <cstring>
#include <cstdint>
#include <type_traits>
/// Macro making `restrict` keyword available
#ifndef restrict
#if defined(__GNUC__) && ((__GNUC__ == 8) || (__GNUC__ == 9) || (__GNUC__ == 10))
#define restrict __restrict__
#else
#warning "restrict keyword not defined via macro"
#endif
#endif
namespace detail {
inline void pack_worker( char * /*dest*/ ) noexcept { }
template< typename SRC, typename ... TAIL > inline void pack_worker( char * restrict dest, const SRC * const src, const TAIL * const ... tail ) noexcept {
static_assert( std::is_trivially_copyable<SRC>::value, "pack cannot handle types which are not trivially copyable" );
memcpy( dest, src, sizeof(SRC) );
pack_worker( dest + sizeof(SRC), tail ... );
}
inline void unpack_worker( const char * /*src*/ ) noexcept { }
template< typename DEST, typename ... TAIL > inline void unpack_worker( const char * restrict src, DEST * dest, TAIL * ... tail ) noexcept {
static_assert( std::is_trivially_copyable<DEST>::value, "unpack cannot handle types which are not trivially copyable" );
memcpy( dest, src, sizeof(DEST) );
unpack_worker( src + sizeof(DEST), tail ... );
}
template< int64_t ... > struct sum_worker : std::integral_constant< int64_t, 0 > { };
template< int64_t X, int64_t ... Xs > struct sum_worker< X, Xs... > : std::integral_constant< int64_t, X + sum_worker<Xs...>::value > { };
}
template< int64_t ... Xs > inline constexpr int64_t sum() { return detail::sum_worker< Xs... >::value; };
template< typename DEST, typename SRC, typename ... TAIL > inline void * pack( DEST & restrict dest, const SRC & src, const TAIL & ... tail ) noexcept {
constexpr size_t sze = sum< sizeof(SRC), sizeof(TAIL)... >();
static_assert( sizeof(DEST) == sze, "Sizes are not compatible. If this is intended, pass `&dest` as `void*`." );
detail::pack_worker( reinterpret_cast<char *>(&dest), &src, &tail ... );
return static_cast< void* >( reinterpret_cast<char *>(dest) + sze );
}
template< typename SRC, typename ... TAIL > inline void * pack( void * restrict dest, const SRC & src, const TAIL & ... tail ) noexcept {
constexpr size_t sze = sum< sizeof(SRC), sizeof(TAIL)... >();
detail::pack_worker( reinterpret_cast<char *>(dest), &src, &tail ... );
return static_cast< void* >( reinterpret_cast<char *>(dest) + sze );
}
template< typename SRC, typename DEST, typename ... TAIL > inline void unpack( const SRC & restrict src, DEST & dest, TAIL & ... tail ) noexcept {
constexpr size_t sze = sum< sizeof(DEST), sizeof(TAIL)... >();
static_assert( sizeof(SRC) == sze, "Sizes are not compatible. If this is intended, pass `&src` as `const void*`." );
detail::unpack_worker( reinterpret_cast<char const *>(&src), &dest, &tail ... );
}
template< typename DEST, typename ... TAIL > inline void unpack( const void * restrict const src, DEST & dest, TAIL & ... tail ) noexcept {
detail::unpack_worker( reinterpret_cast<char const *>(src), &dest, &tail ... );
}
#endif //CSTRINGUTILS_HPP
Here is my set of unit tests. (In my own project, I use googletest, and thus, the tests can be written more concisely.)
int main() {
// GeneralTest
{
int8_t c1 = 0xAA, c2 = 0xBB, c1r, c2r;
int16_t dest;
pack( dest, c1, c2 );
int16_t res = 0xBBAA;
assert( memcmp(&dest, &res, sizeof(dest)) == 0 );
unpack( dest, c1r, c2r );
assert( memcmp(&c1, &c1r, sizeof(c1)) == 0 );
assert( memcmp(&c2, &c2r, sizeof(c2)) == 0 );
assert( memcmp(&dest, &res, sizeof(dest)) == 0 );
}
{
int16_t c1 = 0xAABB, c1r;
int32_t c2 = 0xCCDD'EEFF, c2r;
int8_t c3 = 0x11, c3r;
int8_t c4 = 0x22, c4r;
int64_t tmp;
pack( tmp, c1, c2, c3, c4 );
int64_t res = 0x2211'CCDD'EEFF'AABB;
assert( memcmp(&tmp, &res, sizeof(tmp)) == 0 );
unpack( tmp, c1r, c2r, c3r, c4r );
assert( memcmp(&c1, &c1r, sizeof(c1)) == 0 );
assert( memcmp(&c2, &c2r, sizeof(c2)) == 0 );
assert( memcmp(&c3, &c3r, sizeof(c3)) == 0 );
assert( memcmp(&c4, &c4r, sizeof(c4)) == 0 );
assert( memcmp(&tmp, &res, sizeof(tmp)) == 0 );
}
// TemporaryTest
{
int32_t tmp;
bool t[4];
pack( tmp, true, true, false, true );
unpack( tmp, t[3], t[1], t[2], t[0] ); // strange ordering is on purpose
bool tr = true;
bool fa = false;
assert( memcmp(&t[3], &tr, 1) == 0 );
assert( memcmp(&t[1], &tr, 1) == 0 );
assert( memcmp(&t[0], &tr, 1) == 0 );
assert( memcmp(&t[2], &fa, 1) == 0 );
}
{
uint8_t t[8];
unpack( 0xAABBCCDD, t[0], t[2], t[4], t[6] ); // strange ordering is on purpose
uint8_t r0 = 0xDD, r2 = 0xCC, r4 = 0xBB, r6 = 0xAA;
assert( memcmp(&t[0], &r0, 1) == 0 );
assert( memcmp(&t[2], &r2, 1) == 0 );
assert( memcmp(&t[4], &r4, 1) == 0 );
assert( memcmp(&t[6], &r6, 1) == 0 );
}
// VoidTest
{
char tmp[4];
pack( (void*)(&tmp), true, false, false );
bool tr = true;
bool fa = false;
assert( memcmp(&tmp[0], &tr, 1) == 0 );
assert( memcmp(&tmp[1], &fa, 1) == 0 );
assert( memcmp(&tmp[1], &fa, 1) == 0 );
bool b1, b2, b3;
unpack( (const void*)(&tmp), b1, b2, b3 );
assert( memcmp(&b1, &tr, 1) == 0 );
assert( memcmp(&b2, &fa, 1) == 0 );
assert( memcmp(&b3, &fa, 1) == 0 );
}
// PackTestSizeTest
{
// int8_t c1 = 0xAA, c2 = 0xBB, c3 = 0xCC;
// int16_t tmp;
// pack( tmp, c1, c2, c3 ); // Must not compile
// pack( tmp, c1 ); // Must not compile
}
{
// int tmp;
// pack( tmp, 10, 20 ); // Must not compile
}
}
main()
for convenience. That allows us to review the tests properly, too (e.g. naming, layout, use ofEXPECT_TRUE
instead ofEXPECT_EQ
). I routinely post questions including Google Test cases; nobody has complained yet! \$\endgroup\$