cpx (cpp extra) is a lightweight, header-only C++17 utility library designed to make modern C++ development more ergonomic and composable.
-
Tag-based reflection Unified interface for JSON, TOML, YAML, Protobuf, MessagePack, CLI
-
SQL query builder
-
Composable iterator utilities
iterate,enumerate,zip,map,drop,take,collect -
RAII utilities Scope-exit guard via
defer -
CLI argument parsing
-
Concurrency primitives Channel, queue, and semaphore
-
Inference utilities Support for ONNX Runtime and OpenVINO
-
...and more
cpx is intentionally non-opinionated about third-party dependencies.
It does not bundle or enforce specific libraries, instead it provides adapters so you can integrate with tools you already use.
-
Formatting
fmt -
CLI parsing
CLI11 -
Struct reflection
Boost.PFR -
Enum reflection
magic_enum -
JSON
yyjson,nlohmann::json,rapidjson -
YAML
yaml-cpp -
MessagePack
msgpack -
Protobuf Protocol Buffers
-
Inference runtimes ONNX Runtime, OpenVINO
Contributions are very much welcomed :)
See examples/ for runnable demos:
| Example | Description |
|---|---|
0-basic |
Tag-based serialization/deserialization |
1-struct |
Struct reflection (Boost.PFR) |
2-enum |
Enum reflection (magic_enum) |
3-cli |
CLI argument parsing |
4-sql |
SQLite query builder |
5-protobuf |
Protobuf serialization |
For more detailed usage, check tests/.
You can specialize Reflect<T> for your custom types without littering your public API:
// public API
struct User {
std::string name;
int age;
std::tm created_at;
};
// private API
#include <cpx/reflect.h>
template <>
struct cpx::Reflect<User> : cpx::Fields<
cpx::Reflect<User>, // CRTP
&User::name,
&User::age,
&User::created_at
> {
static constexpr TagInfo name = "name";
static constexpr TagInfo age = "age";
static constexpr TagInfo created_at = "created-at";
static constexpr tags_type tags() {
return std::tie(name, age, created_at);
}
};That’s it. No macros, no codegen, no black magic, no public API pollution — just pure C++17.
You can then use your favorite serializer library:
#include <cpx/yaml/jbeder_yaml.h>
User u = {"Sucipto", 24, now()};
// dump `u` into string
std::string yaml = cpx::jbeder_yaml::dump(u);
// parse from string into `u`
cpx::jbeder_yaml::parse(yaml, u);You can specialize reflection for a specific format.
For example, JSON may prefer camelCase while others use kebab-case:
#include <cpx/json/json.h>
template <>
struct cpx::json::Reflect<User> : cpx::Fields<
cpx::json::Reflect<User>,
&User::name,
&User::age,
&User::created_at
> {
static constexpr TagInfo created_at = "createdAt";
static constexpr tags_type tags() {
using Base = cpx::Reflect<User>;
return std::tie(Base::name, Base::age, created_at);
}
};Now:
cpx::jbeder_yaml::dump(u);
// name: Sucipto
// age: 24
// created-at: ...
cpx::nlohmann_json::dump(u);
// {"name":"Sucipto","age":24,"createdAt":"..."}In this context, reflection may not mean what you expect. It is closer to a literal reflection (or mirroring) of custom types into already known representations such as numbers, strings, tuples, and tagged references.
Reflect<T> : Fields<...> {...} is simply a convenient way to reflect a struct
(or a class with public fields) into a tuple of tagged references.
You can construct one manually:
#include <cpx/fmt.h>
std::string name = "Sucipto";
int age = 24;
TagInfo tag_name = "name";
TagInfo tag_age = "age";
auto u = std::make_tuple(
cpx::tag_tie(name, tag_name),
cpx::tag_tie(age, tag_age)
); // std::tuple<cpx::TagInfoFor<std::string&, cpx::TagInfo&>, cpx::TagInfoFor<int&, cpx::TagInfo&>>
fmt::println("{}", u); // (name="Sucipto", age=24)As the name suggests, Reflect<T> is not limited to reflecting fields. It can also act as a mirror for primitive types.
For example:
template <>
struct cpx::Reflect<MyString> {
static constexpr bool value = true;
using const_type = std::string_view; // for serialization
using type = std::string; // for deserialization
static const_type of(const MyString& str) {
return str.view();
}
static decltype(auto) of(MyString& str) {
// if MyString has mutable reference to std::string
std::string& ref = str.get_mut_str();
return ref;
// otherwise, you may need some kind of proxy type that
// converts string at destructor
struct Proxy {
MyString& str;
std::string proxy;
~Proxy() noexcept(false) {
str = MyString::from_std(proxy);
}
operator std::string&() {
return proxy;
}
};
return Proxy{str};
}
};cpx does not care on how your third-party libraries are organized.
If the header guard matches, cpx will not try to include the default path.
For example:
#include "you/might/locate/nlohmann_json/in/weird/path/json.h"
#include "as/well/as/toml++.h"
#include <cpx/json/nlohmann_json.h>
#include <cpx/toml/marzer_toml.h>For libraries with native ADL serializers (such as fmt or nlohmann::json), cpx provides automatic integration:
fmt::println("{}", u);
nlohmann::json j = u;For libraries with SAX streaming support (avoid building an intermediate DOM), cpx provides a streming interface:
#include <cpx/json/rapid_json.h>
#include <iostream>
User u = {"Sucipto", 24, now()};
std::cout << cpx::rapid_json::io << u << std::endl;Note
cpxis only tested against the third-party versions listed inCMakeLists.txt.
TagInfo stores metadata associated with a reflected field.
By default:
struct TagInfo {
std::string_view key = "";
std::string_view oneof = "";
// CLI
std::string_view short_ = "";
std::string_view env = "";
std::string_view help = "";
// Protobuf / MessagePack
int field_number = 0;
// General serialization behavior
bool omitempty = false; // when serializing, omit zeros, nullopt, and .empty()
bool skipmissing = false; // when deserializing, skip missing field instead of throw
bool noserde = false;
bool positional = false; // for CLI
// Protobuf encoding
bool fixed = false;
bool zigzag = false;
bool packed = true;
};You can construct TagInfo directly from a string literal:
TagInfo key = "key,omitempty,skipmissing";This is equivalent to:
TagInfo key;
key.key = "key";
key.omitempty = true;
key.skipmissing = true;TagInfo user =
"user,"
"field_number=1,"
"omitempty,"
"skipmissing,"
"env=USER,"
"help=Define a username";
TagInfo age =
"age,"
"field_number=2,"
"omitempty,"
"skipmissing,"
"fixed,"
"help=Specify the user age";Fields can belong to a oneof group.
The unique identifier must be shared among all members of the group.
If oneof is specified, it also implies omitempty and skipmissing
TagInfo circle = "circle, oneof=circle|rectangle"; // space after comma is ignored
TagInfo rectangle = "rectangle, oneof=circle|rectangle";String literals are concise but may be typo-prone.
You can use TagInfoBuilder instead.
Both constructor throgh string literals and builder are constexpr friendly tho.
TagInfo user = TagInfoBuilder("user")
.field_number(1)
.omitempty()
.skipmissing()
.env("USER")
.help("Define a username");