Thank you for your interest in contributing to BitSerializer! This guide will help you set up the development environment and understand the project workflow.
We welcome contributions of all kinds:
- Bug fixes — found something broken? A PR with a fix is always appreciated.
- Minor improvements — performance tweaks, code cleanup, better error messages, small enhancements.
- Documentation — typo fixes, improved examples, missing explanations — all valuable.
- Tests — additional test coverage, edge case tests, regression tests for reported issues.
- Performance improvements — optimizations are welcome, but please include benchmark results before and after your changes (see Performance Benchmarks).
Note
For significant changes — new features, architectural refactoring, changes to the public API — please start by creating an ADR (Architecture Decision Record) as a GitHub issue or discussion before submitting a PR. This helps us align on the approach before you invest time in implementation.
An ADR documents a significant architectural decision and its context. When proposing a major change, include the following:
- Title — short descriptive name for the decision (e.g., "Switch from DOM to SAX parser for JSON archive")
- Context — what problem or requirement motivates this change? What constraints exist?
- Decision — what approach are you proposing? Describe it clearly.
- Alternatives considered — what other approaches were evaluated, and why were they rejected?
- Consequences — what are the trade-offs? What becomes easier or harder? Are there breaking changes?
ADRs don't need to be formal or lengthy — a clear, concise explanation is enough. The goal is to have a shared understanding before diving into implementation.
The library aims to support the most widely used serialization formats, not every format that exists. Each new archive adds maintenance burden, and the author's capacity is limited. If you'd like to propose a new format, please open a discussion first — explain the use case, the format's popularity and adoption, and why existing archives don't cover the need.
- C++17 compiler (or newer)
- CMake >= 3.10
- Ninja build system
- VCPKG package manager
- Visual Studio 2022 or 2026 (Community, Professional, or Enterprise)
- Install the "Desktop development with C++" workload
- This includes MSVC, CMake, and Ninja
- Optionally, install the C++ Clang Compiler for Windows component for Clang-cl support
- Install the Test Adapter for Google Test extension (by Microsoft) — enables running GTest tests directly from the VS Test Explorer
- Linux: GCC (8+) or Clang (8+)
- macOS: Xcode Command Line Tools (provides Clang)
- Install Ninja:
# Linux (Debian/Ubuntu) sudo apt-get install build-essential ninja-build cmake # macOS brew install ninja cmake
VCPKG manages third-party dependencies (RapidJSON, PugiXml, RapidYAML, Google Test).
# Clone vcpkg (anywhere on your system)
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
# Bootstrap
# Windows:
.\bootstrap-vcpkg.bat
# Linux/macOS:
./bootstrap-vcpkg.shSet VCPKG_ROOT to point to your vcpkg installation:
Windows (PowerShell):
[System.Environment]::SetEnvironmentVariable("VCPKG_ROOT", "C:\path\to\vcpkg", "User")Tip
This sets the variable permanently (stored in the registry). Restart your terminal or IDE for the change to take effect in already running processes.
Linux/macOS:
# Add to ~/.bashrc or ~/.zshrc:
export VCPKG_ROOT=/path/to/vcpkgThe easiest way on Windows is to use Visual Studio's built-in CMake support:
- Open Visual Studio (2022 or 2026)
- Select File > Open > Folder... and choose the BitSerializer repository root
- Visual Studio will automatically detect
CMakeSettings.jsonand configure the project - Select a configuration from the dropdown (e.g.,
Win-MSVC-x64-Debug) - Build with F7 (or Ctrl+Shift+B, or Build > Build All)
- Run tests with Test > Run All Tests
Tip
VS stores build output in %LOCALAPPDATA%\CMakeBuilds\<GUID>\build\<Config>\ (not in the repository). When running tests or binaries from the command line, use the path shown in the VS Output window, or build from the command line instead (see Options 2/3 below) to get the standard build/ directory.
Available configurations in CMakeSettings.json:
| Configuration | Compiler | Platform | C++ Std | Linkage |
|---|---|---|---|---|
Win-MSVC-x86-Debug/Release |
MSVC | x86 | 17 | static |
Win-MSVC-x64-Debug/Release |
MSVC | x64 | 17 | static |
Win-MSVC-x64-Debug+Analysis |
MSVC | x64 | 17 | static + clang-tidy |
Win-MSVC-x64-Debug-Dynamic |
MSVC | x64 | 17 | shared |
Win-MSVC-x64-CXX20-Debug/Release |
MSVC | x64 | 20 | static |
Win-MSVC-x64-CXX23-Debug/Release |
MSVC | x64 | 23 | static |
Win-Clang-x64-Debug/Release |
Clang-cl | x64 | 17 | static |
Linux-GCC8-x64-Debug/Release |
GCC 8 | x64 | 17 | static |
Linux-GCC12-x64-CXX20-Debug/Release |
GCC 12 | x64 | 20 | static |
Linux-Clang8-x64-Debug/Release |
Clang 8 | x64 | 17 | static |
Linux-Clang14-x64-CXX20-Debug/Release |
Clang 14 | x64 | 20 | static |
Linux-GCC-ARM32-Debug/Release |
GCC (cross) | ARM32 | 17 | static |
Linux-GCC-ARM64-CXX20-Debug/Release |
GCC (cross) | ARM64 | 20 | static |
Linux-GCC-ARM64BE-Debug/Release |
GCC (cross) | ARM64BE | 17 | static |
Note
Linux and ARM configurations require a remote machine connected via SSH. This can be a VM in VirtualBox, WSL, or any Linux machine accessible from your Windows host. Configure the remote connection in Tools > Options > Cross Platform > Connection Manager in Visual Studio.
See also: Linux development with C++ in Visual Studio — setup guide, remote connection, and CMake Linux projects.
Note
The project uses CMakeSettings.json rather than CMakePresets.json. This is intentional — CMake Presets require CMake 3.19+, but the project supports GCC 8 and Clang 8, which ship with older CMake versions (3.10–3.16).
Tip
If VS doesn't pick up CMakeSettings.json when opening the project, check that Tools > Options > CMake > General > Always use CMake Presets is disabled. When enabled, VS ignores CMakeSettings.json and may auto-create a default CMakePresets.json.
Use the pre-configured VS developer environment — open one of these from the Start Menu:
Visual Studio (2022/2026) > Visual Studio Tools > VC >
x64 Native Tools Command Prompt for VS (build for x64)
x64_x86 Cross Tools Command Prompt for VS (build for x86 from x64 host)
These prompts have MSVC, CMake, and Ninja already in PATH. Then:
cd <path-to-bitserializer>
:: CMake configure
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release ^
-DBUILD_TESTS=ON -DBUILD_SAMPLES=ON -DBUILD_BENCHMARKS=ON ^
-DBUILD_JSON_ARCHIVE=ON -DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON ^
-DBUILD_RAPIDJSON_ARCHIVE=ON -DBUILD_PUGIXML_ARCHIVE=ON -DBUILD_RAPIDYAML_ARCHIVE=ON
:: Build
ninja -C build
:: Run tests
ctest --test-dir build -T test --output-on-failure -j2Using Clang-cl instead of MSVC:
From the same VS developer command prompt, add -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl to the CMake command:
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl ^
-DBUILD_TESTS=ON -DBUILD_SAMPLES=ON -DBUILD_BENCHMARKS=ON ^
-DBUILD_JSON_ARCHIVE=ON -DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON ^
-DBUILD_RAPIDJSON_ARCHIVE=ON -DBUILD_PUGIXML_ARCHIVE=ON -DBUILD_RAPIDYAML_ARCHIVE=ONcd <path-to-bitserializer>
# Configure
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=ON -DBUILD_SAMPLES=ON -DBUILD_BENCHMARKS=ON \
-DBUILD_JSON_ARCHIVE=ON -DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON \
-DBUILD_RAPIDJSON_ARCHIVE=ON -DBUILD_PUGIXML_ARCHIVE=ON -DBUILD_RAPIDYAML_ARCHIVE=ON
# Build
ninja -C build
# Run tests
ctest --test-dir build -T test --output-on-failure -j2Using a specific compiler:
# GCC 14
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 \
...
# Clang 15
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang-15 -DCMAKE_CXX_COMPILER=clang++-15 \
...Add -DSTATIC_ANALYSIS_CLANG_TIDY=ON to any CMake configure command (Windows, Linux, or macOS) to run clang-tidy during the build:
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Debug \
-DSTATIC_ANALYSIS_CLANG_TIDY=ON \
-DBUILD_TESTS=ON -DBUILD_JSON_ARCHIVE=ON \
-DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON
ninja -C buildAdd -DENABLE_COVERAGE=ON to a Debug build (GCC/Clang only; rejected on MSVC/clang-cl) to
instrument with --coverage, then run the tests and generate the report with the coverage
target (requires gcovr):
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON \
-DBUILD_TESTS=ON -DBUILD_JSON_ARCHIVE=ON \
-DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON
ninja -C build
ctest --test-dir build --output-on-failure
ninja -C build coverage # writes build/coverage/coverage.xml and build/coverage/html/The report is limited to library code (include/bitserializer/ and src/); tests, samples,
benchmarks and VCPKG dependencies are excluded. CI publishes it from the Ubuntu_GCC_Coverage
Preflight job.
Important
All archive options are OFF by default — you must explicitly enable what you need.
| Option | Default | Description |
|---|---|---|
BUILD_TESTS |
OFF | Build test executables |
BUILD_SAMPLES |
OFF | Build sample projects |
BUILD_BENCHMARKS |
OFF | Build performance benchmarks |
BUILD_MICROBENCHMARKS |
OFF | Build intra-binary A/B microbenchmarks |
BUILD_JSON_ARCHIVE |
OFF | Built-in JSON archive (no external dependency) |
BUILD_CSV_ARCHIVE |
OFF | Built-in CSV archive (no external dependency) |
BUILD_MSGPACK_ARCHIVE |
OFF | Built-in MsgPack archive (no external dependency) |
BUILD_RAPIDJSON_ARCHIVE |
OFF | JSON archive via RapidJSON |
BUILD_PUGIXML_ARCHIVE |
OFF | XML archive via PugiXml |
BUILD_RAPIDYAML_ARCHIVE |
OFF | YAML archive via RapidYAML |
BUILD_SHARED_LIBS |
OFF | Build as shared/DLL library |
STATIC_ANALYSIS_CLANG_TIDY |
OFF | Enable clang-tidy during build |
ENABLE_COVERAGE |
OFF | Instrument with --coverage and add the coverage target (GCC/Clang only) |
CMAKE_CXX_STANDARD |
17 | C++ standard (17, 20, 23, or 26) |
Note
Selecting CMAKE_CXX_STANDARD=26 requires CMake 3.30 or newer. MSVC has no C++26 mode: with MSVC, 23 maps to /std:c++latest, which is the closest available mode.
If your changes may affect performance (serialization speed or output size), please verify that there is no significant regression by running the benchmarks.
Build the benchmark executable in Release mode (benchmarks in Debug mode use minimal test time — only enough to verify functionality):
:: Windows (from VS Developer Command Prompt)
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_BENCHMARKS=ON -DBUILD_JSON_ARCHIVE=ON ...
ninja -C build archives_benchmarks
:: Run (from command-line build)
build/bin/archives_benchmarks.exe --list
build/bin/archives_benchmarks.exe --bench bs-json,rapidjson --stages save,load --stage-time 10In Release mode, each archive and stage runs for 30 seconds to produce repeatable results. The benchmark also warms up the CPU for 10 seconds before starting. Use --bench, --stages, --stage-time and --warmup to run a subset without rebuilding; see benchmarks/archives/README.md for the full CLI and the run_repeated.py helper for repeated runs.
Important
Avoid using the system during the test for accurate results. Close other applications and background processes that may cause fluctuations.
- Pins the process to a single CPU core and sets maximum priority to minimize fluctuations
- Compares BitSerializer archives against raw usage of underlying libraries (RapidJSON, PugiXml, RapidYAML, nlohmann-json)
- Measures serialization speed (fields/ms) for save and load operations
- Uses a representative flat model (5 numeric and 5 string fields); see Test model for the intent and balance
- Reports are saved as JSON files in
benchmark_results/directory (requires the built-in JSON archive,BUILD_JSON_ARCHIVE=ON)
Performance changes must be validated with the full round-trip benchmark (save → load of a representative model), not only with microbenchmarks of individual functions. Compiler code generation can shift for reasons unrelated to the algorithm you changed:
- A real case: after templating the JSON reader on the format (
ArchiveType::Json/Jsonc), strict-JSON load regressed ~6% (16500 → ~15493 fields/ms) even though the strict path was unchanged —if constexprremoved all JSONC handling and the hot functions' assembly was byte-identical to before. The cause was purely compiler behavior: MSVC generated slower code for the strict instantiation when the larger JSONC instantiation was compiled in the same translation unit. Splitting the explicit instantiations into separate TUs restored the performance.
Lessons:
- Measure the round-trip. A change that looks neutral in isolation can still move the whole pipeline.
- Microbenchmarks of a single function are still useful for profiling and hypothesis testing (e.g. whether
__forceinline/noinlinehelps a hot loop), but they cannot detect whole-TU/binary codegen effects — never rely on them alone. - Compare in a single session. Machine state drifts between sessions; take several back-to-back runs (min-time per metric) of the configurations you are contrasting.
- Don't shorten the run. Keep the default stage time (30 s per stage). Cutting it to a few seconds sharply increases run-to-run variance on a busy machine, so the min-time results become unreliable and can even flip the sign of a small (few %) difference.
For function-level questions — "is std::to_chars faster than std::to_string here?", "does __forceinline help this loop?" — use the intra-binary A/B harness in benchmarks/microbenchmarks/.
It compiles the base and variant implementations into the same binary and measures them alternately in one process. Because both share the build's code layout, the measured difference is not affected by the layout lottery — this makes it reliable even on MSVC, where function alignment is unavailable. A conservative noise band (block-to-block spread of the delta) is reported next to each delta, and a delta is only flagged as significant when it exceeds that band.
cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_MICROBENCHMARKS=ON -DBUILD_JSON_ARCHIVE=ON ^
-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
ninja -C build microbenchmark
build\bin\microbenchmark.exeMicrobenchmarks answer "which implementation of this function is faster", not "is the whole pipeline faster" — still validate final changes with the round-trip benchmark above. See the harness README for the API and recommendations.
The harness also covers the full archive Save/Load pipeline on a small model (see the archive A/B section) — useful for A/B of two archives or two implementations without the heavy round-trip run.
For the recommended optimization protocol (function-level A/B → archive-level A/B → round-trip validation), see the A/B workflow.
The benchmark reports the minimum time over millions of single-call samples. The minimum is an extreme-value statistic, so it is very sensitive to the alignment of the hot functions: a benign code change anywhere in the linked image — even code that never runs in the timed region — can shift them by one cache line and move the reported speed by ~5-10%. This is a "layout lottery", not a real regression. It can be reproduced by adding a dummy function to a hot translation unit and rebuilding: the unchanged algorithm then reports a different min.
To keep builds comparable, the benchmark target aligns its functions to 64 bytes (-mllvm -align-all-functions=6 on Clang, -falign-functions=64 -falign-loops=32 on GCC), which collapses the spread to ~1-2%. This is applied to the benchmark target only; the library build is untouched (function alignment has no ABI impact, so it stays compatible with any DLL/static build).
Rule: run authoritative A/B benchmarks with Clang or GCC.
- Clang/GCC can freeze function alignment; MSVC (
cl.exe) cannot (no per-function alignment attribute or flag), so its spread stays at ~7% and small deltas are masked by layout noise. - On Windows,
clang-cl(ships with Visual Studio) is enough — no separate toolchain needed: add-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-clto the CMake command. - Absolute numbers differ between compilers — only compare deltas measured with the same compiler and flags.
- MSVC runs remain useful as a smoke check, but treat deltas below ~7% as noise.
Exceptions are the primary error-handling mechanism by design. An experimental migration of the
Convert submodule to Expected-based error handling showed a regression in real serialization
benchmarks (CSV save −9%, load −5%): the overhead is paid on every successful call, whereas
exceptions are zero-cost on the happy path and cost only on rare error paths. Expected/error codes
are therefore not used in the hot serialization code.
Key directories:
include/bitserializer/— public API headerssrc/— implementations for built-in archives (CSV, JSON, MsgPack)tests/unit_tests/— per-module unit teststests/integration_tests/— per-archive end-to-end testssamples/— 15 example projectsdocs/— per-format documentationbenchmarks/archives/— round-trip serialization benchmarks vs competing librariesbenchmarks/harness/— shared benchmark infrastructure (perf utils, A/B harness, process runner)benchmarks/microbenchmarks/— intra-binary A/B microbenchmark harness (see itsREADME.md)
The library uses an archive pattern: each format provides input_archive_type (deserialization) and output_archive_type (serialization). BitSerializer::LoadObject<TArchive>() / SaveObject<TArchive>() dispatch values through Detail::Dispatch(), which routes fundamentals, STD containers, KeyValue pairs, and custom classes to the correct Serialize() overload.
The code example below serializes a User object — the diagram that follows visualizes the same flow step by step:
struct User
{
int Id = 0;
std::string Name;
std::vector<std::string> Tags;
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("Id", Id);
archive << KeyValue("Name", Name);
archive << KeyValue("Tags", Tags);
}
};
// Usage:
std::string output;
BitSerializer::SaveObject<JsonArchive>(user, output);sequenceDiagram
participant User
participant UserObj as UserObject.Serialize()
participant API as BitSerializer API
participant Dispatch
participant Serialize as Serialize()
participant Root as Root Scope
participant ObjScope as Object Scope
participant ArrScope as Array Scope
User->>API: SaveObject<TArchive>(userObj, output)
API->>Root: output_archive_type(output, context)
API->>Dispatch: Dispatch(rootScope, userObj)
Dispatch->>Serialize: Serialize(rootScope, userObj)
Note over Serialize: For classes with Serialize() method:<br/>1. CountMapObjectFields(archive, userObj)<br/>2. archive.OpenObjectScope(mapSize)
Serialize->>Root: rootScope.OpenObjectScope(mapSize)
Root-->>Serialize: ObjectScope (opened)
Serialize->>UserObj: userObj.Serialize(objectScope)
Note over Root: Root scope has the same contract<br/>as child scopes (SerializeValue,<br/>OpenArrayScope, OpenObjectScope...).<br/>Compile-time validation works for all scopes.
UserObj->>Dispatch: objectScope << KeyValue("Id", id)<br/>(operator<< calls Dispatch)
Dispatch->>Serialize: Serialize(objectScope, "Id", id)
Serialize->>ObjScope: objectScope.SerializeValue("Id", id)
UserObj->>Dispatch: objectScope << KeyValue("Items", vec)<br/>(operator<< calls Dispatch)
Dispatch->>Serialize: Serialize(objectScope, "Items", vec)
Serialize->>ObjScope: objectScope.OpenArrayScope("Items", size)
ObjScope-->>Serialize: ArrayScope (opened)
loop for each element
Serialize->>ArrScope: arrScope.SerializeValue(elem)
end
Note over Root: Child scopes destroyed via RAII.<br/>Finalize() is called on root only.
Root->>API: rootScope.Finalize()
API-->>User: output string/stream
A scope is a short-lived RAII object responsible for serializing the current node (object, array, or binary blob) in the archive. It does not own child scopes — each nested node creates its own scope, and when all nested elements are serialized, the parent scope's destructor finalizes the node. Root, object, and array scopes each implement the same core interface. Scopes inherit from both the archive traits and ArchiveScope<SerializeMode>, which provides GetMode(), IsSaving(), IsLoading(), GetContext(), GetOptions(), and the opened/not-opened state (IsOpened(), operator bool).
Open*Scope() returns the concrete scope by value. A scope is either opened or not opened: when the node cannot be opened (for example a type mismatch while loading), the returned scope is in the not-opened state, so IsOpened()/operator bool yield false and the scope performs no operation on destruction.
| Method | Scope types | Mode | Description |
|---|---|---|---|
IsOpened() / operator bool |
All | Both | Whether the scope was successfully opened |
GetPath() |
All | Both | Current path in archive (for error messages) |
SerializeValue(T& value) |
Root, Array, Binary | Both | Serialize next value |
SerializeValue(TKey&& key, T& value) |
Object | Both | Serialize value by key |
OpenArrayScope(...) |
Root, Array, Object | Both | Open nested array |
OpenObjectScope(...) |
Root, Array, Object | Both | Open nested object |
OpenBinaryScope(...) |
Root, Array, Object | Both | Open nested binary (if supported) |
Finalize() |
Root only | Both | Called after serialization completes |
GetEstimatedSize() |
Array, Object, Binary | Load | Element count for container reserve |
IsEnd() |
Array, Binary | Load | true when all elements read |
VisitKeys(TCallback&&) |
Object | Load | Enumerates all keys (re-seeks to start) |
All scopes, including root, implement the same contract — the combination of methods (SerializeValue, OpenArrayScope, OpenObjectScope) which enables compile-time format validation. For example, CSV root only has OpenArrayScope() (no SerializeValue()), so serializing a primitive at root level is a compile error. See serialization_detail/serialization_dispatch.h and the respective archive headers for details.
These rules are part of the archive contract and are locked by the shared test helpers
(TestSkippedArrayValues, TestMismatchedArrayValue, TestMismatchedTypesPolicy in
src/testing_tools/common_test_methods.h).
Array scopes (sequences) map one archive slot to one target slot, regardless of whether the value was successfully converted:
SerializeValue()/Open*Scope()consume the current slot even when the value is skipped — anullvalue or a value rejected byMismatchedTypesPolicy::Skip/OverflowNumberPolicy::Skip.- The slot counter used by
IsEnd()(and, for text formats, the read of the next value separator) is advanced for every consumed slot, so a skipped value never shifts the following values. - The target slot is left untouched: an element of a fixed C-array or of a pre-filled container keeps
its previous value; a newly created element of a dynamic container stays default-constructed.
Skipmeans "ignore the value", not "remove/compact the container element" (arity is preserved). - A fixed-size array must consume exactly
Nslots; a mismatch between the number of slots in the archive and the target size is reported asSerializationException(OutOfRange). - Nested scopes follow the same rule: a skipped nested array/object consumes one parent slot.
Example: loading [null, 5] into int[2] (or std::vector<int>) leaves the first element unchanged
(or default-initialized) and loads 5 into the second one.
Object scopes (key-value maps) are key-driven:
SerializeValue(key, value)/Open*Scope(key, ...)look up the key by name; the order of keys in the archive is irrelevant. Unknown keys are skipped.- If the key is missing, the target is left unchanged and the call returns
false; useKeyValue(..., Required())to report a validation error instead. - A value with a mismatched type follows
MismatchedTypesPolicy:ThrowError(the default) throws,Skipleaves the target unchanged and consumes the key-value pair. - Null values are excluded from
MismatchedTypesPolicyand are simply skipped (same as in arrays). - Remaining key-value pairs are skipped when the object scope is destroyed, so a partially loaded object does not desynchronize the archive.
The top-level archive (e.g., MsgPackArchive) is a type alias that specializes ArchiveBase with the archive's traits and root scope classes:
using MsgPackArchive = ArchiveBase<
Detail::MsgPackArchiveTraits,
Detail::MsgPackReadRootScope,
Detail::MsgPackWriteRootScope
>;ArchiveBase inherits the traits struct and provides:
- Traits constants (
archive_type,key_type,is_binary,require_array_size, etc.) input_archive_type— root scope class for deserializationoutput_archive_type— root scope class for serialization
When in doubt, follow the patterns you see in the surrounding code.
- C++17 minimum standard
- Clang-tidy is configured (
.clang-tidy) with all warnings treated as errors - Tabs for indentation
#pragma oncefor include guards
| Element | Convention | Examples |
|---|---|---|
| Classes / Structs (incl. class templates) | CamelCase, no T prefix |
ArchiveScope, SerializationOptions, JsonWriteObjectScope |
| Interfaces | I + CamelCase |
IJsonReader, ICsvReader |
| Public/protected methods | CamelCase | ReadValue(), OpenArray(), GetPosition() |
| Private methods | CamelCase | ParseNextLine(), UnescapeValue() |
| Private member variables | m + CamelCase |
mPos, mLineNumber, mInputData |
| Public struct members | camelCase | name, object, index |
| Constructor parameters | camelCase with in prefix to avoid shadowing |
inValue, inName |
| Local variables / Constants | camelCase | codepoint, hexVal, targetValue |
| Template parameters | T + CamelCase |
TSource, TTarget, TArchive |
| Type aliases (public, in traits/classes) | snake_case_type |
value_type, key_type, input_archive_type |
| Type aliases (internal) | snake_case |
archive_string_view, msgpack_variable_key |
| Macros | BITSERIALIZER_ + SCREAMING_SNAKE_CASE |
BITSERIALIZER_HAS_FILESYSTEM, BITSERIALIZER_REGISTER_ENUM |
| Namespaces | CamelCase | BitSerializer::Json::Detail |
Note
Legacy C-prefixed aliases (CRawTime, CTimeRef) are deprecated in favor of UnixTime and TimeRef; new code must use the non-prefixed names.
The T prefix marks a template parameter (a placeholder), not a template class. Concrete types keep plain CamelCase even when they are class templates (e.g. ArchiveScope<TMode>); non-template types never take the T prefix either (e.g. ScopeUnopened).
- Always use braces for
if/else/for/whileblocks - Opening brace on a separate line:
if (ch == 'n' && mInputData.compare(mPos + 1, 3, "ull", 3) == 0) { mPos += 4; return true; }
- For single-statement blocks, it is acceptable to put the opening brace on the same line:
if (hexVal == 0xFF) { throw ParsingException("Invalid hex digit", lineNumber, pos); }
elseon its own line:if (codepoint < 0x80) { buffer += static_cast<char>(codepoint); } else if (codepoint < 0x800) { buffer += static_cast<char>(0xC0 | (codepoint >> 6)); }
- Doxygen-style comments in English for public API:
/** @brief ... */ - MIT license header required on all source files:
/******************************************************************************* * Copyright (C) 2018-2026 by Pavel Kisliak * * This file is part of BitSerializer library, licensed under the MIT license. * *******************************************************************************/
- Include order: license header,
#pragma once, standard library, third-party, project headers - Use
[[nodiscard]]on const getters,noexceptwhere appropriate
Use the format: [Component] Short description (#issue)
Use imperative mood ("Add feature", not "Added feature"). If the commit relates to a GitHub issue, add the issue number in parentheses at the end. Omit if there's no related issue.
Components:
[Core]— Core serialization framework[Json]— Built-in JSON archive[CSV]— CSV archive[MsgPack]— MsgPack archive[RapidJson]— RapidJSON-based archive[Convert]— String conversion submodule[Tests]— Tests[CI]— CI/CD pipeline[Docs]— Documentation
Examples:
[Json] Add support for reading surrogate pairs
[CSV] Optimize deserialization performance (+22% reading from memory)
[Convert] Improve float-to-string precision
[RapidJson, PugiXml, RapidYaml] Fix loading optional object and array from `Null` (e.g. "myValue": null) (#11)
- Rebase on latest
devbefore opening a PR. If new commits appear ondevwhile your PR is open, rebase again. - Keep PR history clean — squash or amend commits locally before pushing when it makes sense.
- The project prefers a linear history (no merge commits). PRs are merged via rebase.
- If AI-assisted tools were used (code generation, refactoring, analysis), please include a note
AI-assisted, manually verified & adaptedin the PR description indicating that the output was reviewed and adjusted by a human.
The project uses Azure DevOps for CI. The pipeline runs automatically on all pushes and PRs.
- Preflight — Clang-tidy, Valgrind memory checks, and the GCC Debug build that publishes test results and code coverage (runs first)
- Build and test — 16 configurations across Windows, Linux, macOS, and ARM
The CI matrix covers:
- Windows: MSVC (x86/x64, static/dynamic), Clang-cl (x64, Release and Debug)
- Linux: GCC 9, GCC 14 (C++23, C++26), Clang 11, Clang 18
- macOS: Clang 14 (C++17, C++20)
- ARM: arm32, arm64, arm64be (cross-compiled, tested with QEMU)
When reporting bugs, please include:
- BitSerializer version (check
include/bitserializer/config.h) - Compiler and version
- Operating system and architecture
- Minimal code example that reproduces the issue
- Expected vs actual behavior