Skip to content

Commit e20b5cb

Browse files
committed
Add trait deduction for serializables
Add compile-time asserts for serializable traits
1 parent f4ab4fe commit e20b5cb

File tree

5 files changed

+252
-7
lines changed

5 files changed

+252
-7
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,13 @@ These examples can also be seen in [`src/test/examples_test.cpp`](https://github
348348
# Extensibility
349349
The library is made with extensibility in mind.
350350
The `bit_writer` and `bit_reader` use a template trait specialization of the given type to deduce how to serialize and deserialize the object.
351+
The only requirements of the trait is that it has (or can deduce) 2 static functions which take a bit_writer& and a bit_reader& respectively as their first argument.
352+
The 2 functions must also return a bool indicating whether the serialization was a success or not, but can otherwise take any number of additional arguments.
351353
The general structure of a trait looks like the following:
352354

353355
```cpp
354356
template<>
355-
struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
357+
struct serialize_traits<TRAIT_TYPE> // The type to use when referencing this specific trait
356358
{
357359
// Will be called when writing the object to a stream
358360
static bool serialize(bit_writer& stream, ...)

include/bitstream/stream/bit_reader.h

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "../utility/assert.h"
33
#include "../utility/crc.h"
44
#include "../utility/endian.h"
5+
#include "../utility/meta.h"
56

67
#include "byte_buffer.h"
78
#include "serialize_traits.h"
@@ -39,7 +40,7 @@ namespace bitstream
3940
* @param bytes The byte array to read from. Should be 4-byte aligned if possible. The size of the array must be a multiple of 4
4041
* @param num_bytes The maximum number of bytes that we can read
4142
*/
42-
bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
43+
explicit bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
4344
m_Buffer(static_cast<const uint32_t*>(bytes)),
4445
m_NumBitsRead(0),
4546
m_TotalBits(num_bytes * 8),
@@ -309,18 +310,40 @@ namespace bitstream
309310
}
310311

311312
/**
312-
* @brief Reads from the buffer, using the given `Trait`.
313+
* @brief Reads from the buffer, using the given @p Trait.
314+
* @note The Trait type in this function must always be explicitly declared
313315
* @tparam Trait A template specialization of serialize_trait<>
314316
* @tparam ...Args The types of the arguments to pass to the serialize function
315317
* @param ...args The arguments to pass to the serialize function
316318
* @return Whether successful or not
317319
*/
318320
template<typename Trait, typename... Args>
319-
bool serialize(Args&&... args) noexcept(noexcept(serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...)))
321+
bool serialize(Args&&... args) noexcept(utility::is_noexcept_serialize_v<Trait, bit_reader, Args...>)
320322
{
323+
static_assert(utility::has_serialize_v<Trait, bit_reader, Args...>, "Could not find serializable trait for the given type. Remember to specialize serializable_traits<> with the given type");
324+
321325
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
322326
}
323327

328+
/**
329+
* @brief Reads from the buffer, by trying to deduce the trait.
330+
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
331+
* If the trait cannot be deduced it will not compile.
332+
* @tparam Trait A template specialization of serialize_trait<>
333+
* @tparam ...Args The types of the arguments to pass to the serialize function
334+
* @param ...args The arguments to pass to the serialize function
335+
* @return Whether successful or not
336+
*/
337+
template<typename Trait, typename... Args>
338+
bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_noexcept_serialize_v<utility::deduce_trait_t<Trait, bit_reader, Args...>, bit_reader, Trait, Args...>)
339+
{
340+
using deduce_t = utility::deduce_trait_t<Trait, bit_reader, Args...>;
341+
342+
static_assert(utility::has_serialize_v<deduce_t, bit_reader, Trait, Args...>, "Could not deduce serializable trait for the given arguments. Remember to specialize serializable_traits<> with the given type");
343+
344+
return serialize_traits<deduce_t>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
345+
}
346+
324347
private:
325348
const uint32_t* m_Buffer;
326349
uint32_t m_NumBitsRead;

include/bitstream/stream/bit_writer.h

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "../utility/assert.h"
33
#include "../utility/crc.h"
44
#include "../utility/endian.h"
5+
#include "../utility/meta.h"
56

67
#include "byte_buffer.h"
78
#include "serialize_traits.h"
@@ -44,7 +45,7 @@ namespace bitstream
4445
* @param bytes The byte array to write to. Should be 4-byte aligned if possible
4546
* @param num_bytes The number of bytes in the array. Must be a multiple of 4
4647
*/
47-
bit_writer(void* bytes, uint32_t num_bytes) noexcept :
48+
explicit bit_writer(void* bytes, uint32_t num_bytes) noexcept :
4849
m_Buffer(static_cast<uint32_t*>(bytes)),
4950
m_NumBitsWritten(0),
5051
m_TotalBits(num_bytes * 8),
@@ -354,18 +355,40 @@ namespace bitstream
354355
}
355356

356357
/**
357-
* @brief Writes to the buffer, using the given `Trait`.
358+
* @brief Writes to the buffer, using the given @p Trait.
359+
* @note The Trait type in this function must always be explicitly declared
358360
* @tparam Trait A template specialization of serialize_trait<>
359361
* @tparam ...Args The types of the arguments to pass to the serialize function
360362
* @param ...args The arguments to pass to the serialize function
361363
* @return Whether successful or not
362364
*/
363365
template<typename Trait, typename... Args>
364-
bool serialize(Args&&... args) noexcept(noexcept(serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...)))
366+
bool serialize(Args&&... args) noexcept(utility::is_noexcept_serialize_v<Trait, bit_writer, Args...>)
365367
{
368+
static_assert(utility::has_serialize_v<Trait, bit_writer, Args...>, "Could not find serializable trait for the given type. Remember to specialize serializable_traits<> with the given type");
369+
366370
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
367371
}
368372

373+
/**
374+
* @brief Writes to the buffer, by trying to deduce the trait.
375+
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
376+
* If the trait cannot be deduced it will not compile.
377+
* @tparam ...Args The types of the arguments to pass to the serialize function
378+
* @tparam Trait A template specialization of serialize_trait<>
379+
* @param ...args The arguments to pass to the serialize function
380+
* @return Whether successful or not
381+
*/
382+
template<typename Trait, typename... Args>
383+
bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_noexcept_serialize_v<utility::deduce_trait_t<Trait, bit_writer, Args...>, bit_writer, Trait, Args...>)
384+
{
385+
using deduce_t = utility::deduce_trait_t<Trait, bit_writer, Args...>;
386+
387+
static_assert(utility::has_serialize_v<deduce_t, bit_writer, Trait, Args...>, "Could not deduce serializable trait for the given arguments. Remember to specialize serializable_traits<> with the given type");
388+
389+
return serialize_traits<deduce_t>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
390+
}
391+
369392
private:
370393
uint32_t* m_Buffer;
371394
uint32_t m_NumBitsWritten;

include/bitstream/utility/meta.h

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#pragma once
2+
3+
#include "../stream/serialize_traits.h"
4+
5+
#include <type_traits>
6+
7+
namespace bitstream::utility
8+
{
9+
// Check if type has a serializable trait
10+
template<typename Void, typename T, typename Stream, typename... Args>
11+
struct has_serialize : std::false_type {};
12+
13+
template<typename T, typename Stream, typename... Args>
14+
struct has_serialize<std::void_t<decltype(serialize_traits<T>::serialize(std::declval<Stream&>(), std::declval<Args>()...))>, T, Stream, Args...> : std::true_type {};
15+
16+
template<typename T, typename Stream, typename... Args>
17+
constexpr bool has_serialize_v = has_serialize<void, T, Stream, Args...>::value;
18+
19+
20+
// Check if type is noexcept, if it exists
21+
template<typename Void, typename T, typename Stream, typename... Args>
22+
struct is_noexcept_serialize : std::false_type {};
23+
24+
template<typename T, typename Stream, typename... Args>
25+
struct is_noexcept_serialize<std::enable_if_t<has_serialize_v<T, Stream, Args...>>, T, Stream, Args...> :
26+
std::bool_constant<noexcept(serialize_traits<T>::serialize(std::declval<Stream&>(), std::declval<Args>()...))> {};
27+
28+
template<typename T, typename Stream, typename... Args>
29+
constexpr bool is_noexcept_serialize_v = is_noexcept_serialize<void, T, Stream, Args...>::value;
30+
31+
32+
// Get the underlying type without &, &&, * or const
33+
template<typename T>
34+
using base_t = typename std::remove_const_t<std::remove_pointer_t<std::decay_t<T>>>;
35+
36+
37+
// Meta functions for guessing the trait type from the first argument
38+
template<typename Void, typename Trait, typename Stream, typename... Args>
39+
struct deduce_trait
40+
{
41+
using type = void;
42+
};
43+
44+
// Non-const value
45+
template<typename Trait, typename Stream, typename... Args>
46+
struct deduce_trait<std::enable_if_t<
47+
!std::is_pointer_v<std::decay_t<Trait>>&&
48+
has_serialize_v<base_t<Trait>, Stream, Trait, Args...>>,
49+
Trait, Stream, Args...>
50+
{
51+
using type = base_t<Trait>;
52+
};
53+
54+
// Const value
55+
template<typename Trait, typename Stream, typename... Args>
56+
struct deduce_trait<std::enable_if_t<
57+
!std::is_pointer_v<std::decay_t<Trait>>&&
58+
has_serialize_v<std::add_const_t<base_t<Trait>>, Stream, Trait, Args...>>,
59+
Trait, Stream, Args...>
60+
{
61+
using type = std::add_const_t<base_t<Trait>>;
62+
};
63+
64+
// Non-const pointer
65+
template<typename Trait, typename Stream, typename... Args>
66+
struct deduce_trait<std::enable_if_t<
67+
std::is_pointer_v<std::decay_t<Trait>>&&
68+
has_serialize_v<std::add_pointer_t<base_t<Trait>>, Stream, Trait, Args...>>,
69+
Trait, Stream, Args...>
70+
{
71+
using type = std::add_pointer_t<base_t<Trait>>;
72+
};
73+
74+
// Const pointer
75+
template<typename Trait, typename Stream, typename... Args>
76+
struct deduce_trait<std::enable_if_t<
77+
std::is_pointer_v<std::decay_t<Trait>>&&
78+
has_serialize_v<std::add_pointer_t<std::add_const_t<base_t<Trait>>>, Stream, Trait, Args...>>,
79+
Trait, Stream, Args...>
80+
{
81+
using type = std::add_pointer_t<std::add_const_t<base_t<Trait>>>;
82+
};
83+
84+
template<typename Trait, typename Stream, typename... Args>
85+
using deduce_trait_t = typename deduce_trait<void, Trait, Stream, Args...>::type;
86+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#include "../shared/assert.h"
2+
#include "../shared/test.h"
3+
4+
#include <bitstream/stream/bit_reader.h>
5+
#include <bitstream/stream/bit_writer.h>
6+
#include <bitstream/utility/bits.h>
7+
8+
#include <bitstream/traits/integral_traits.h>
9+
#include <bitstream/traits/quantization_traits.h>
10+
#include <bitstream/traits/string_traits.h>
11+
12+
namespace bitstream::test::deduction
13+
{
14+
BS_ADD_TEST(test_deduce_integrals)
15+
{
16+
// Create a writer, referencing the buffer and its size
17+
byte_buffer<4> buffer;
18+
bit_writer writer(buffer);
19+
20+
// Write the value
21+
int32_t value = -45; // We can choose any value within the range below
22+
BS_TEST_ASSERT(writer.serialize(value, -90, 40)); // A lower and upper bound which the value will be quantized between
23+
24+
// Flush the writer's remaining state into the buffer
25+
uint32_t num_bytes = writer.flush();
26+
27+
BS_TEST_ASSERT_OPERATION(num_bytes, <= , 4);
28+
29+
// Create a reader by moving and invalidating the writer
30+
bit_reader reader(buffer, num_bytes);
31+
32+
// Read the value back
33+
int32_t out_value; // We don't have to initialize it yet
34+
BS_TEST_ASSERT(reader.serialize(out_value, -90, 40)); // out_value should now have a value of -45
35+
36+
BS_TEST_ASSERT_OPERATION(out_value, == , value);
37+
}
38+
39+
BS_ADD_TEST(test_deduce_chars)
40+
{
41+
// Create a writer, referencing the buffer and its size
42+
byte_buffer<32> buffer;
43+
bit_writer writer(buffer);
44+
45+
// Write the value
46+
const char* value = "Hello world!";
47+
BS_TEST_ASSERT(writer.serialize(value, 32U)); // The second argument is the maximum size we expect the string to be
48+
49+
// Flush the writer's remaining state into the buffer
50+
uint32_t num_bytes = writer.flush();
51+
52+
// Create a reader by moving and invalidating the writer
53+
bit_reader reader(buffer, num_bytes);
54+
55+
// Read the value back
56+
char out_value[32]; // Set the size to the max size
57+
BS_TEST_ASSERT(reader.serialize(out_value, 32U)); // out_value should now contain "Hello world!\0"
58+
59+
BS_TEST_ASSERT(strcmp(out_value, value) == 0);
60+
}
61+
62+
BS_ADD_TEST(test_deduce_strings)
63+
{
64+
// Create a writer, referencing the buffer and its size
65+
byte_buffer<32> buffer;
66+
bit_writer writer(buffer);
67+
68+
// Write the value
69+
std::string value = "Hello world!";
70+
BS_TEST_ASSERT(writer.serialize(value, 32U)); // The second argument is the maximum size we expect the string to be
71+
72+
// Flush the writer's remaining state into the buffer
73+
uint32_t num_bytes = writer.flush();
74+
75+
// Create a reader by moving and invalidating the writer
76+
bit_reader reader(buffer, num_bytes);
77+
78+
// Read the value back
79+
std::string out_value; // The string will be resized if the output doesn't fit
80+
BS_TEST_ASSERT(reader.serialize(out_value, 32U)); // out_value should now contain "Hello world!"
81+
82+
BS_TEST_ASSERT_OPERATION(out_value, == , value);
83+
}
84+
85+
BS_ADD_TEST(test_deduce_bounded_range)
86+
{
87+
// Create a writer, referencing the buffer and its size
88+
byte_buffer<4> buffer;
89+
bit_writer writer(buffer);
90+
91+
// Write the value
92+
bounded_range range(1.0f, 4.0f, 1.0f / 128.0f); // Min, Max, Precision
93+
float value = 1.2345678f;
94+
writer.serialize(range, value);
95+
96+
// Flush the writer's remaining state into the buffer
97+
uint32_t num_bytes = writer.flush();
98+
99+
BS_TEST_ASSERT_OPERATION(num_bytes, <= , 4);
100+
101+
// Create a reader by moving and invalidating the writer
102+
bit_reader reader(buffer, num_bytes);
103+
104+
// Read the value back
105+
float out_value;
106+
reader.serialize(range, out_value); // out_value should now be a value close to 1.2345678f
107+
108+
BS_TEST_ASSERT_OPERATION(std::abs(value - out_value), <= , range.get_precision());
109+
BS_TEST_ASSERT_OPERATION(range.get_bits_required(), < , 32);
110+
}
111+
}

0 commit comments

Comments
 (0)