Library / SDK
felixguendling/cista avatar
felixguendling/cista

cista: C++ serialization by reading your structs as they sit in memory

Cista is a simple, high-performance, zero-copy C++ serialization & reflection library.

2,256 stars159 forksC++MIT

At a glance

What is it?
A single-header C++17 library that serializes native structs without macros or code generation, at the cost of a format that only your own program can read.
Who is it for?
cista is the right choice when both ends of the wire are C++17, the two processes agree on pointer width, and the data is read far more often than it is written, because that is exactly the shape its offset mode is built for. Asset loading, graph files, shared memory between your own components and cached network payloads all fit.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One header, no macros, and reflection you did not write

The claim at the top of the README is narrow and specific: single header, no dependencies, no macros, no source code generation. The whole library is downloaded as `cista.h` from the latest release, and that file is the entire dependency list. Nothing in `CMake/`, `fuzz/` or `tools/` has to be present for you to use it.

What replaces macros is a reflection mechanism, and the README is upfront that it is useful beyond serialization. The first example is the shape to read first because it is the smallest complete one:

cpp
namespace data = cista::raw;
struct my_struct {
  int a_{0};
  struct inner {
      data::string b_;
  } j;
};

std::vector<unsigned char> buf;
{
  my_struct obj{1, {data::string{"test"}}};
  buf = cista::serialize(obj);
}

auto deserialized = cista::deserialize<my_struct>(buf);

Note what is not there. No macro above the struct, no registration call, no IDL file, no generated header. A plain aggregate is serializable because the library walks the members at runtime. Strings are not `std::string` but `data::string`, which matters for the size discussion below, and the `raw` namespace selects one of two serialization modes.

The same derivation also produces hashing and equality. The README lists automatic deduction of hash and equality among the ways it reduces boilerplate, and the advanced example shows a `hash_map` keyed on a user struct with no hand-written hash function. Whether you want that is a separate question from whether you want the serialization.

Raw mode copies, offset mode hands you pointers into the buffer

The two namespaces, `cista::raw` and `cista::offset`, are the central design decision and the benchmark table makes the difference plain. In the table from the README, `raw` serialization takes 3555 ms against 72 ms for `offset`, and the resulting size is 176.4M against 25.3M. That is roughly a sevenfold difference in both directions on the same input.

The mechanism is what explains the numbers. Offset mode writes pointer fields as integer offsets relative to the start of the buffer, so one contiguous block of bytes holds the whole object graph and a pointer in the data is just an index into it. Raw mode writes the actual pointer values, which means the buffer cannot be relocated, and deserialization has to walk and fix everything up. The payoff is that `offset` output can be memory mapped and handed straight to code that indexes into it, which is what the README means by save 50% memory when serializing directly to the filesystem with no intermediate buffer.

The advanced example writes a hash map to a memory mapped file, with versioning and a check sum switched on:

cpp
namespace data = cista::offset;
constexpr auto const MODE =
    cista::mode::WITH_VERSION | cista::mode::WITH_INTEGRITY;

struct pos { int x, y; };
using pos_map =
    data::hash_map<data::vector<pos>,
                   data::hash_set<data::string>>;

Those two mode flags are worth understanding separately from the namespace choice. `WITH_VERSION` enables the built-in optional versioning through recursive type hashing, and `WITH_INTEGRITY` adds a check sum so corrupt data is rejected rather than read. Both are opt-in, which means the safe default is off, and you have to remember to turn them on for data that matters.

Hash containers and the non-aggregate escape hatch

The container story is a deliberate part of the design rather than an add-on. The README describes a serializable high-performance hash map and hash set based on Google's Swiss Table, and notes that deserialized data supports modification and resizing. That last point is the one to sit with: this is not a read-only view you are stuck with, because the whole point of offset mode is that you can cast the buffer to your type and keep working.

Non-aggregate types are handled through a member function rather than a macro. The advanced example covers derived classes and classes with custom constructors:

cpp
struct parent {
  parent() = default;
  explicit parent(int a) : x_{a}, y_{a} {}
  auto cista_members() { return std::tie(x_, y_); }
  int x_, y_;
};
struct child : parent {
  child() = default;
  explicit child(int a) : parent{a}, z_{a} {}
  auto cista_members() {
    return std::tie(*static_cast<parent*>(this), z_);
  }
  int z_;
};

`cista_members()` returns a tie of members, and for the derived class it includes a cast to the base, so the hierarchy is captured explicitly. The cost is that you write this function by hand and you are responsible for keeping it accurate as the class changes. This is the one place where the no-boilerplate claim gives way, and it is a reasonable trade: the mistake is a visible function in your source rather than a stale generated file.

The derived type then works as a hash map key, and the README notes that de/serialization, hashing, equality and the type hash are all defaulted for you from that one function.

Pointer width and endianness are the two portability rules

The use cases section is short and contains the constraint that decides whether the library fits your problem: reader and writer should have the same pointer width. Loading data on systems with a different byte order is supported, so endianness is handled, but a 32-bit build and a 64-bit build disagree about the size of every pointer field in the format.

This is the practical boundary of the whole design. Because there is no schema and no field numbering, the format is the memory layout, and memory layout is platform-specific. If your writer is a 64-bit desktop tool and your reader is a 32-bit embedded target, the same file is not readable, and no flag fixes it.

The README also says that only C++17 software can read or write data today, while noting it should be possible to generate accessors for other programming languages too. Read that as a stated intention rather than a feature. Combined with the pointer width rule, the practical consequence is that a cista file is a cache or transport format between components you control, not a file format for a product with a long shelf life and a mixed fleet of readers.

The fuzzing note is worth registering too. The README claims the library is checked through continuous fuzzing using LLVM's LibFuzzer, and there is a `fuzz/` directory to match. That is the right kind of testing for a parser that trusts bytes, and it is a stronger signal than the benchmark table.

Reading the benchmark table honestly

The README reproduces a comparison table across Cap'n Proto, cereal, cista in both modes, and Flatbuffers, with columns for serialize, deserialize, fast deserialize, traverse, deserialize and traverse, and size. The repository points at a separate benchmark repository for more detail.

Two entries deserve a second look. Cap'n Proto shows 0.002 ms for deserialize, which beats cista's 0.053 ms, and a 50.5M size against cista's 25.3M. Cista does not win everywhere, and the table says so. What cista wins is serialize time, the combined deserialize and traverse column at 132 ms, and size. Cap'n Proto's combined figure is 353 ms. So the honest reading is that cista competes well when you have to write a lot and then walk the result, and that a project whose access pattern is dominated by random reads of a fixed file is looking at a table where Cap'n Proto already does better.

The raw mode row also shows the cost of the wrong mode: 176.4M and 68.900 ms deserialize. The two modes are not two styles of the same thing, they are different formats with a sevenfold gap, and picking the namespace is the first real decision you make.

Worth noting that this table is the project's own comparison against a set of competitors it selected. Treat the ranking as indicative and go look at the benchmark repository if the numbers are going into a decision.

Where the README stops and the wiki takes over

The README is a good overview and an incomplete manual. It gives you the single-header download, three worked examples, the feature list, the benchmark table, and the use cases. Everything else is linked out to the wiki: installation and usage, a serialization reference, custom de/serialization functions, data structure versioning, hash containers and a hashing framework.

The custom de/serialization page matters more than its position in the list suggests. The built-in reflection covers structs, containers and the container types it ships, but the moment you have a type from another library, a bitfield, or a type whose layout is not what you want on disk, that page is where you find out how to write your own functions. The versioning page is the other one to read, because `WITH_VERSION` is only useful if you know what the recursive type hash is actually protecting you from.

The project is MIT licensed, compatible with Clang, GCC and MSVC, requires C++17, and has CI workflows for Linux and Windows. Releases are infrequent: v0.16 on 2025-09-06, v0.15 on 2024-04-05, v0.14 on 2023-06-02, with the last push on 2026-09-27. A version line that moves a few times a year suits a library you vendor as one header file, which is also how you should consume it. Pin the header.

Editorial conclusion

cista is the right choice when both ends of the wire are C++17, the two processes agree on pointer width, and the data is read far more often than it is written, because that is exactly the shape its offset mode is built for. Asset loading, graph files, shared memory between your own components and cached network payloads all fit. It is the wrong choice the moment a second language or an older build of your own program has to read the data, since there is no schema evolution story and no accessor generation for other languages today. The last release was v0.16 on 2025-09-06 and the last push was on 2026-09-27, so the pace is slow but not abandoned. Start with the single header from the latest release and the `offset` example, then read the pointer width note in the use cases section before you design a file format around it.

Frequently asked questions

Is Cista compatible with other programming languages?

Not today. The README states that currently only C++17 software can read and write cista data, and that generating accessors for other languages should be possible but is not implemented. If another language has to read the data, the README points you to Protocol Buffers, Cap'n Proto, Flatbuffers, cereal, MessagePack and others instead.

Does Cista support schema evolution and downward compatibility?

Not in the sense other formats offer it. Because the serialized form follows the C++ memory layout, there are no field numbers to fall back on. Cista offers optional automatic versioning through recursive type hashing via the `WITH_VERSION` mode, and the README lists protocol evolution as a reason to choose a different library.

What is the difference between cista::raw and cista::offset?

Offset mode writes pointers as offsets into the buffer, so the result is one contiguous relocatable block that can be memory mapped. Raw mode writes real pointer values, which means the buffer cannot be moved and deserialization has to fix up pointers. The README's benchmark table shows offset mode producing a much smaller file far faster than raw mode.

Does Cista need code generation or macros?

No. The README describes it as a single header with no dependencies, no macros and no source code generation. Plain aggregate structs are handled by runtime reflection, and non-aggregate types such as derived classes opt in by writing a `cista_members()` function that returns the members to serialize.

What pointer width does Cista require for reading and writing?

The README says the reader and writer should have the same pointer width, while loading data on systems with a different byte order is supported. So a 32-bit and a 64-bit build do not share a format, and the reader also needs to be C++17.

Official sources

  1. felixguendling/cista on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/felixguendling-cista.svg)](https://hysenlabs.com/projects/felixguendling-cista)