Add std::variant serialization support#13
Add std::variant serialization support#13hoshinohikari wants to merge 2 commits intoPavelKisliak:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class std::variant support to BitSerializer and expands test coverage across unit + multiple archive integration suites, along with a few related build/tooling and serialization correctness tweaks.
Changes:
- Introduce
std::variantserialization as an object{ index, value }and add unit/integration tests for it. - Extend test fixture generation to build
std::variantvalues. - Adjust build/static-analysis CMake behavior and make small correctness fixes in supporting components (e.g., RapidJSON scope handling, UTF conversion, refine/noexcept, file path forwarding).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
include/bitserializer/types/std/variant.h |
New std::variant serialization implementation (index + value). |
tests/unit_tests/std_types_tests/std_types_tests.cpp |
Unit tests covering several std::variant alternatives + member usage. |
tests/integration_tests/rapidjson_archive_tests/rapidjson_archive_tests.cpp |
Integration tests for std::variant with RapidJSON archive. |
tests/integration_tests/rapidyaml_archive_tests/rapidyaml_archive_tests.cpp |
Integration tests for std::variant with RapidYAML archive. |
tests/integration_tests/pugixml_archive_tests/pugixml_archive_tests.cpp |
Integration tests for std::variant with PugiXML archive. |
tests/integration_tests/msgpack_archive_tests/msgpack_archive_tests.cpp |
Integration tests for std::variant with MsgPack archive. |
src/testing_tools/auto_fixture.h |
Add fixture builder for std::variant. |
include/bitserializer/rapidjson_archive.h |
Adjust key handling in save-mode scope opening (object/array). |
tests/unit_tests/CMakeLists.txt |
Temporarily disable clang-tidy while configuring unit test subdirs. |
tests/integration_tests/CMakeLists.txt |
Temporarily disable clang-tidy while configuring integration test subdirs. |
CMakeLists.txt |
Temporarily disable clang-tidy while configuring benchmarks/samples subdirs. |
src/testing_tools/common_test_entities.h |
Add NOLINT for value-param; minor formatting change. |
include/bitserializer/refine.h |
Tweak noexcept and assignability static_assert for fallback handling. |
include/bitserializer/conversion_detail/convert_utf.h |
Iterator/utility tweaks (includes + parameter passing + moves). |
include/bitserializer/bit_serializer.h |
Avoid forwarding path into APIs used multiple times (prevents moved-from issues). |
include/alloca.h |
Add Windows compatibility shim for <alloca.h>. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| auto& insertedMember = FindMember(memberKey)->value; | ||
| return std::make_optional<RapidJsonArrayScope<TMode, TEncoding, TAllocator>>(&insertedMember, mAllocator, this->GetContext(), this, memberKey); |
There was a problem hiding this comment.
Same lifetime issue as OpenObjectScope: memberKey is a local string that is used to initialize parentKey (string_view) inside the returned array scope. This can leave mParentKey dangling and cause UB later (e.g., in GetPath()). Ensure the scope stores an owning key (or otherwise guarantees lifetime) before storing a string_view.
| auto& insertedMember = FindMember(memberKey)->value; | |
| return std::make_optional<RapidJsonArrayScope<TMode, TEncoding, TAllocator>>(&insertedMember, mAllocator, this->GetContext(), this, memberKey); | |
| auto insertedMemberIt = FindMember(memberKey); | |
| auto& insertedMemberValue = insertedMemberIt->value; | |
| const auto* insertedMemberKey = insertedMemberIt->name.GetString(); | |
| return std::make_optional<RapidJsonArrayScope<TMode, TEncoding, TAllocator>>(&insertedMemberValue, mAllocator, this->GetContext(), this, insertedMemberKey); |
| public: | ||
| TestClassWithSubTypes() = default; | ||
| TestClassWithSubTypes(Args... args) | ||
| TestClassWithSubTypes(Args... args) // NOLINT(performance-unnecessary-value-param) |
There was a problem hiding this comment.
This constructor is indented inconsistently compared to the surrounding methods (extra leading tab), which looks accidental and makes the class harder to read. Align it with the other members’ indentation.
| TestClassWithSubTypes(Args... args) // NOLINT(performance-unnecessary-value-param) | |
| TestClassWithSubTypes(Args... args) // NOLINT(performance-unnecessary-value-param) |
| if (value.index() != TIndex) | ||
| { | ||
| value.template emplace<TIndex>(); | ||
| } | ||
| Serialize(archive, key, std::get<TIndex>(value)); |
There was a problem hiding this comment.
When loading, this code switches the active alternative via value.emplace<TIndex>() without constructor args. That silently imposes a requirement that every alternative that might be loaded is default-constructible; otherwise compilation fails with a hard-to-diagnose error. Consider adding a clear static_assert documenting this requirement (or refactoring to deserialize into a temporary and only emplace/assign on success, if you want to support non-default-constructible alternatives).
| else if (archive.GetOptions().mismatchedTypesPolicy == MismatchedTypesPolicy::ThrowError) | ||
| { | ||
| throw SerializationException(SerializationErrorCode::MismatchedTypes, | ||
| "Variant index is out of range for target type: " + Convert::ToString(activeIndex)); | ||
| } |
There was a problem hiding this comment.
New behavior: when loading and mismatchedTypesPolicy == ThrowError, an out-of-range variant index throws SerializationException(MismatchedTypes, ...). There are tests for mismatched tuple sizes, but no tests added here to assert this variant-specific error path. Add a unit/integration test that loads a variant with an invalid index and verifies the exception behavior (and optionally a Skip-policy case).
| @@ -0,0 +1,13 @@ | |||
| /******************************************************************************* | |||
There was a problem hiding this comment.
Most headers under include/ appear to use the standard BitSerializer copyright/MIT license banner at the top. This new compatibility header doesn’t include that banner, which makes licensing/attribution inconsistent—consider adding the standard header block here too.
| /******************************************************************************* | |
| /******************************************************************************* | |
| * BitSerializer - Core serialization library * | |
| * * | |
| * Copyright (c) BitSerializer contributors * | |
| * Distributed under the MIT license. See LICENSE file in the project root for * | |
| * full license information. * | |
| *******************************************************************************/ | |
| /******************************************************************************* |
| const auto memberKey = Convert::To<key_type>(key); | ||
| SaveJsonValue(memberKey, RapidJsonNode(rapidjson::kObjectType)); | ||
| auto& insertedMember = FindMember(memberKey)->value; | ||
| return std::make_optional<RapidJsonObjectScope<TMode, TEncoding, TAllocator>>(&insertedMember, mAllocator, this->GetContext(), this, memberKey); |
There was a problem hiding this comment.
In save mode, memberKey is a local key_type and is passed to the child scope constructor as parentKey (a std::string_view). The returned scope stores this view (mParentKey), so it will dangle after OpenObjectScope returns, leading to UB when building paths / error messages. Pass a non-dangling view (e.g., derived directly from the original key input when it has stable storage) or make the scope own a copy of the key and store a view into that owned storage.
|
Hi @hoshinohikari, Thank you so much for this PR! The serialization of Since serializing template <typename T>
class VariantAsTagged
{
public:
explicit VariantAsTagged(T& v) : value(v) {}
T& value;
};
template <typename TArchive, typename... TArgs>
void SerializeObject(TArchive& archive, VariantAsTagged<std::variant<TArgs...>>& value)
{
}In this case, the usage would look like this: std::variant<int, std::string, CUser> data;
archive << KeyValue("data", VariantAsTagged(data));(This is the same pattern as |
| if(STATIC_ANALYSIS_CLANG_TIDY) | ||
| set(_BITSERIALIZER_SAVED_CXX_CLANG_TIDY "${CMAKE_CXX_CLANG_TIDY}") | ||
| set(CMAKE_CXX_CLANG_TIDY "") | ||
| endif() |
There was a problem hiding this comment.
Since the core of BitSerializer is a header-only, the test files are the primary place where template code gets instantiated and analyzed. Please use inline suppressions for false positives.
| @@ -0,0 +1,13 @@ | |||
| /******************************************************************************* | |||
There was a problem hiding this comment.
As far as I understand this was added to fix issue with compiling RapidYaml archive on clang-cl, looks like this is already addressed by author.
| namespace AutoFixtureDetail | ||
| { | ||
| template <size_t TIndex = 0, typename... TArgs> | ||
| void BuildVariantFixture(std::variant<TArgs...>& value, size_t activeIndex) | ||
| { | ||
| if constexpr (TIndex < sizeof...(TArgs)) | ||
| { | ||
| if (activeIndex == TIndex) | ||
| { | ||
| value.template emplace<TIndex>(); | ||
| ::BuildFixture(std::get<TIndex>(value)); | ||
| } | ||
| else | ||
| { | ||
| BuildVariantFixture<TIndex + 1>(value, activeIndex); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @brief Builds a test fixture for `std::variant` value. | ||
| * | ||
| * Randomly selects one of the alternatives and initializes it. | ||
| */ | ||
| template <typename ...TArgs> | ||
| [[maybe_unused]] static void BuildFixture(std::variant<TArgs...>& value) | ||
| { | ||
| static_assert(sizeof...(TArgs) > 0); | ||
| const size_t activeIndex = static_cast<size_t>(std::rand()) % sizeof...(TArgs); | ||
| AutoFixtureDetail::BuildVariantFixture(value, activeIndex); | ||
| } | ||
|
|
There was a problem hiding this comment.
Currently CI fails due to unknown override of BuildFixture(), you can fix it by moving this code to the end of file.
No description provided.