Zero-annotation C++ serialization library for CSV and Parquet, powered by static reflection (Boost.PFR).
Define plain structs — no macros, no code generators, no base classes. tablio derives field names, types, and counts at compile time, then reads/writes tabular files automatically.
- Zero boilerplate — plain aggregate structs, no annotations required
- Compile-time reflection — field names and types resolved via
boost::pfr - CSV — header-aware reader/writer with column-name matching (order-independent)
- Parquet — automatic Arrow schema generation with ZSTD compression
- Array support —
std::vector<T>,std::array<T,N>, and C-style arrays (pipe-delimited in CSV) - Expandable columns — customize how array fields map to CSV columns (collapsed or expanded)
- Enum support — automatic conversion via underlying type
#include "tablio/io.h"
struct Person {
int id;
std::string name;
double score;
};
// Write
std::vector<Person> people = {{1, "Alice", 95.5}, {2, "Bob", 87.3}};
io::CsvWriter<Person>::write("people.csv", people);
io::ParquetWriter<Person>::write("people.parquet", people);
// Read (column-name matching, order-independent)
auto from_csv = io::CsvReader<Person>::read("people.csv");
auto from_pq = io::ParquetReader<Person>::read("people.parquet");| Dependency | Version | Notes |
|---|---|---|
| C++ | ≥ 20 | C++23 recommended |
| Apache Arrow | ≥ 10.0 | libarrow.a, libparquet.a and bundled deps |
| Boost.PFR | 2.2.0 | Header-only, bundled in pfr/ |
include_directories(tablio/ tablio/pfr/)
target_link_libraries(your_target
${ARROW_LIBS} ${PARQUET_LIBS}
)g++ -std=c++23 -I tablio/ -I tablio/pfr/ \
your_code.cpp \
-lparquet -larrow -larrow_bundled_dependencies \
-lsnappy -lre2 -lpthread -ldl -lz| C++ Type | CSV Serialization | Arrow / Parquet Type |
|---|---|---|
int |
std::from_chars / std::format |
int32 |
long long |
std::from_chars / std::format |
int64 |
float |
std::from_chars / std::format |
float32 |
double |
std::from_chars / std::format |
float64 |
bool |
"1" / "true" ↔ 0 / 1 |
boolean |
std::string |
direct | utf8 |
enum |
via underlying type | same as underlying |
std::vector<T> |
pipe-delimited (|) |
list<T> |
std::array<T, N> |
pipe-delimited (|) |
list<T> |
T[N] (C array) |
pipe-delimited (|) |
list<T> |
T can be any of the scalar types above. Unsupported types produce compile-time errors.
Array fields are pipe-delimited in CSV by default:
struct Trade {
int id;
std::vector<int> prices; // "100|200|300" in CSV
std::array<float, 3> factors; // "0.1|0.2|0.3" in CSV
};Flatten std::array fields into multiple CSV columns instead of a single pipe-delimited column:
struct Tick {
int id;
std::array<float, 5> askPrices;
std::array<float, 5> bidPrices;
};
// Expand askPrices into askPrices1..askPrices5, keep bidPrices collapsed
template <>
struct io::field_handler<Tick, 1> : io::expanded_array_handler<Tick, 1> {};Override column naming entirely for full control:
template <>
struct io::field_handler<CustomTick, 1> {
using field_type = std::array<float, 5>;
static constexpr std::size_t column_count = 5;
static std::string column_name(std::size_t i) {
return std::format("price_{}", i + 1);
}
static std::string to_string(const CustomTick& obj, std::size_t i) {
return io::field_to_string(boost::pfr::get<1>(obj)[i]);
}
static void from_string(CustomTick& obj, std::string_view sv, std::size_t i) {
io::string_to_field(sv, boost::pfr::get<1>(obj)[i]);
}
};Structs must be simple aggregate types:
- No base classes
- No user-defined constructors (or
= default) - No
constmembers - No reference members
- No C-style arrays for
get_namesupport (usestd::array<T, N>instead) - Defined at global or namespace scope (not in anonymous namespaces or function-local)
static void write(const std::filesystem::path& path, const std::vector<T>& data);Writes a header row with field names, then one row per record. Numeric types use std::format("{}", val).
static std::vector<T> read(const std::filesystem::path& path);Reads the header row to map column names to struct fields (order-independent). Missing columns keep default values. Numeric types use std::from_chars.
// Default options (ZSTD level 6, 1M rows per row group)
static void write(const std::filesystem::path& path, const std::vector<T>& data);
// Custom options
static void write(const std::filesystem::path& path, const std::vector<T>& data,
const ParquetOptions& opts);Derives Arrow schema from field types at compile time, builds arrays, and writes Parquet.
| Field | Type | Default | Description |
|---|---|---|---|
compression |
parquet::Compression::type |
ZSTD |
Compression codec (ZSTD, SNAPPY, GZIP, LZ4, etc.) |
compression_level |
int |
6 |
Compression level (codec-dependent) |
row_group_size |
int64_t |
1048576 (1M) |
Rows per row group — larger values improve compression and scan efficiency for large files |
Example:
io::ParquetOptions opts;
opts.compression = parquet::Compression::ZSTD;
opts.compression_level = 9;
opts.row_group_size = 5 * 1024 * 1024; // 5M rows per batch
io::ParquetWriter<Tick>::write("large.parquet", data, opts);static std::vector<T> read(const std::filesystem::path& path);Opens a Parquet file, matches columns by name, and reads all rows into structs.
Struct definition
│
├── boost::pfr::tuple_size_v<T> → field count (compile time)
├── boost::pfr::get_name<I, T>() → field names (compile time)
├── boost::pfr::tuple_element_t → field types (compile time)
└── boost::pfr::for_each_field() → field iteration (runtime)
MIT