Using FlatCityBuf with C++
Table of contents
- Dependencies
- Building and installing
- Reading a local file
- Spatial queries
- Attribute queries
- Reading over HTTP
- Bringing your own transport
- Writing files
- Examples
- Verification
The C++ library is a from-scratch native C++17 implementation that reads and writes FlatCityBuf. It replaces the earlier CXX-bridge bindings over the Rust core: there is no Rust toolchain to install, no generated bridge source to compile, no async runtime, and — unless you ask for the HTTP adapter — no TLS dependency.
Source and full documentation: src/cpp.
Dependencies
| Dependency | Required? | Why |
|---|---|---|
flatbuffers | yes | the on-disk format |
nlohmann-json | with FCB_WITH_JSON=ON (the default) | CityJSON emission |
libcurl | with FCB_WITH_CURL=ON (default OFF) | HTTP range requests |
doctest | with FCB_BUILD_TESTS=ON (the default) | tests only, never installed |
# macOS
brew install flatbuffers nlohmann-json doctest
# Debian / Ubuntu
sudo apt-get install libflatbuffers-dev nlohmann-json3-dev doctest-dev
Building and installing
git clone https://github.com/cityjson/flatcitybuf.git
cd flatcitybuf/src/cpp
cmake -B build -S .
cmake --build build
cmake --install build --prefix /your/prefix
Useful options: -DFCB_WITH_CURL=ON (HTTP support), -DFCB_WITH_JSON=OFF (drop CityJSON emission and the nlohmann dependency), -DFCB_BUILD_TESTS=OFF, -DFCB_BUILD_EXAMPLES=OFF.
Then, from your own CMake project:
find_package(flatcitybuf CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE flatcitybuf::flatcitybuf)
That is the whole integration — the FlatBuffers headers are generated and committed, so consumers never need flatc.
Reading a local file
#include <fcb/cityjson.hpp>
#include <fcb/reader.hpp>
#include <iostream>
int main() {
fcb::FcbReader reader = fcb::FcbReader::open_file("delft.fcb");
const auto& info = reader.header().info();
std::cout << info.features_count << " features, CityJSON "
<< info.cityjson_version << ", " << info.crs << "\n";
// One CityJSONSeq metadata line, then one CityJSONFeature per line.
std::cout << fcb::to_cityjson_metadata(reader.header()).dump() << "\n";
auto it = reader.select_all();
while (it.next()) {
std::cout << fcb::to_cityjson_feature(it.current(), reader.header()).dump() << "\n";
}
}
to_cityjson_metadata and to_cityjson_feature return nlohmann::json, so field access is the ordinary nlohmann API: .at("k"), .value("k", default), .contains("k"), .get<double>().
Spatial queries
auto it = reader.select_bbox({84227.77, 445377.33, 85323.23, 446334.69});
while (it.next()) {
std::cout << fcb::to_cityjson_feature(it.current(), reader.header())["id"] << "\n";
}
Attribute queries
select_attr uses the static B+trees, so the column must have been indexed at write time. The comparison value is a typed KeyValue and its type must match the column’s type on disk — a mismatch does not throw, it reinterprets bytes.
#include <fcb/stree.hpp>
fcb::AttrQuery query = {
{"b3_h_dak_50p", fcb::Operator::Gt, fcb::KeyValue::from_f64(20.0)},
};
auto it = reader.select_attr(query);
while (it.next()) {
// 4 of 1115 features match in the Delft example file
}
String columns are indexed on keys truncated to 50 bytes (100 for JSON/binary columns), so the index returns candidates; the default AttrQueryOptions verify each one against the fully decoded attribute. Pass {true} to skip verification — faster, and wrong for long strings.
Reading over HTTP
Build with -DFCB_WITH_CURL=ON:
#include <fcb/http/curl_range_reader.hpp>
auto transport = std::make_shared<fcb::CurlRangeReader>(
"https://storage.googleapis.com/flatcitybuf/3dbag_all_index.fcb");
fcb::FcbReader reader = fcb::FcbReader::open(transport);
auto it = reader.select_bbox({120000, 486000, 121000, 487000});
Only the intersecting features are fetched. On the published 3DBAG file (~68GB, 10.7M features) the example program opens the file in 2 HTTP requests and answers a 1km bounding box in 37.
Bringing your own transport
fcb::RangeReader is the library’s only IO seam — implement it to read from an object store, a game-engine VFS, an mmap, memory, or a decrypting layer:
class MyReader : public fcb::RangeReader {
std::uint64_t total_size() override { /* ... */ }
std::vector<std::uint8_t> read(std::uint64_t offset, std::uint64_t length) override { /* ... */ }
// Optionally override read_batch() to pipeline or multiplex.
};
The interface is deliberately synchronous: batching, not asynchrony, is the concurrency primitive, and a blocking interface is trivially wrapped by whatever threading model your application already has. Read the contract comment in include/fcb/range_reader.hpp before implementing one.
Writing files
fcb::FcbWriter writes .fcb from CityJSON-shaped JSON, with no Rust toolchain involved. Its output is validated byte-for-byte against files written by the Rust writer.
#include <fcb/writer/attribute.hpp>
#include <fcb/writer/fcb_writer.hpp>
// `cj` is the CityJSONSeq metadata line; the schemas must already describe
// every feature that will be added, so scan all features first (column
// numbering is insertion order, exactly as the Rust CLI does it).
fcb::FcbWriter writer(cj, options, attr_schema, semantic_attr_schema);
for (const auto& feature : features) {
writer.add_feature(feature); // spooled to a temp file, not kept in memory
}
std::ofstream out("city.fcb", std::ios::binary);
writer.write(out); // streams header, indices and features straight to `out`
add_feature spools each encoded feature to a private temporary file and write(std::ostream&) streams the finished file out in fixed-size chunks, so memory stays bounded regardless of dataset size. (There is also a write() overload returning a std::vector<std::uint8_t>; it is a convenience for small files and does not have that property.)
Examples
The repository ships eight self-contained example programs, one per capability — see src/cpp/examples, which documents the exact output of each:
| Program | Shows |
|---|---|
fcb_inspect_header | header only: extent, CRS, transform, and which columns are queryable |
fcb_read_local | the whole file (or a bbox) as CityJSONSeq |
fcb_to_cityjson | the CityJSON representation, and how to reach into its fields |
fcb_query_attributes | attribute queries through the B+tree |
fcb_read_features | raw feature access, without CityJSON conversion |
fcb_custom_reader | implementing fcb::RangeReader yourself |
fcb_read_http | remote reads over HTTP range requests |
fcb_write_cityjson | writing a CityJSONSeq out as .fcb |
fcb_custom_reader is the one that makes the format’s argument concrete: on the Delft file, reading everything costs 7 reads and 90.7% of the bytes, while a bounding-box query costs 4 reads and 31.7% of the bytes for 170 of 1115 features.
Verification
The reader’s output is compared against the Rust reader on the full Delft fixture (all 1115 features, compared as parsed JSON trees), plus the shared conformance corpus — single-feature files, prefix-colliding strings, duplicate keys, zero-area extents, geometry templates, appearance at every nesting depth. The test suite runs clean under ASan and UBSan.