Get started / C++
docuconf for C++
You keep your CLI11 app. docuconf adds a Declaration next to it: add_var binds each environment variable to a C++ variable, the C++ type picks the contract type, and the app exports its own contract with --docuconf-export.
- Builds on
- CLI11
- Package
- docuconf (CMake FetchContent)
- Requires
- C++17, CMake 3.16, OpenSSL 3
- Status
- v0.1 alpha, not yet released · conformance 134 of 134
- Source
- docuconf-cpp · the orders example
Not published yet
main branch. After the first release it will be GIT_TAG v0.1.0 in the same FetchContent_Declare.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
There is no release tag yet, so add the main branch with CMake's FetchContent (C++17, CMake 3.16+). CLI11, nlohmann/json, RE2, yaml-cpp and toml++ are taken from your project or the system, or fetched; install OpenSSL 3 for the TLS and keystore checks.
cmake -S . -B build && cmake --build buildinclude(FetchContent)
# Until the first release: the main branch (pin a commit SHA for reproducible builds).
FetchContent_Declare(docuconf
GIT_REPOSITORY https://github.com/Docuconf/docuconf-cpp.git
GIT_TAG main)
FetchContent_MakeAvailable(docuconf)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE docuconf::docuconf)2.Declare your configuration
Next to the CLI11 app, a docuconf::Declaration named after the service and one add_var(name, target, description) per variable. The C++ type picks the contract type (std::chrono::milliseconds is a duration, std::vector<std::string> a list), and the builder adds range, values, secret, required, schemes and min_items. .doc() takes a Doxygen comment: its first paragraph is the description and the rest the details.
CLI::App app{"orders: a small HTTP service configured with docuconf"};
// The service name becomes the contract's metadata.name.
docuconf::Declaration config{app, "orders"};
// Each variable is read from its environment variable and checked at
// boot; --help lists them all. PORT is also a command-line flag (--port)
// for local runs; the platform only ever sets the environment.
int port = 0;
config.add_var("PORT", port, "HTTP listen port").range(1, 65535).default_val(8080).flag();
std::string log_level;
config.add_var("LOG_LEVEL", log_level, "Minimum log level emitted")
.values({"debug", "info", "warn", "error"})
.default_val("info");
// A secret: never printed, never given a default, supplied by the
// platform from a Kubernetes Secret.
std::string database_url;
config.add_var("DATABASE_URL", database_url, "Primary Postgres connection string")
.secret()
.required()
.schemes({"postgres"})
.max_length(2048);
std::vector<std::string> allowed_origins;
config.add_var("ALLOWED_ORIGINS", allowed_origins, "Origins allowed to call the API (CORS)")
.min_items(1)
.default_val({"http://localhost:3000"});
std::chrono::milliseconds request_timeout{};
config.add_var("REQUEST_TIMEOUT", request_timeout, "Time allowed to read a request")
.range("1s", "5m")
.default_val("30s");
int worker_count = 0;
config.add_var("WORKER_COUNT", worker_count)
.doc(R"(
/// Number of request worker threads.
///
/// Each worker answers one connection at a time, so this is also the
/// number of requests served at once. Raise it when requests queue up.
///
/// Keep it at or below the database pool size:
/// @li one connection per worker;
/// @li plus one for migrations.
)")
.range(1, 64)
.default_val(4);3.Load it at boot
DOCUCONF_PARSE is CLI11_PARSE plus every docuconf check: it parses the command line, reads and checks every variable, and binds the values, or exits.
// Parses, validates every input and binds the values, or exits:
// 0 after --help or --docuconf-export, 1 with every violation listed,
// 2 for a mistake in the declaration.
DOCUCONF_PARSE(config, argc, argv);4.Run it, and see an error
Run the example with a valid environment:
cmake -S . -B build -G Ninja && cmake --build build --target orders && DATABASE_URL=postgres://orders:pw@localhost:5432/orders ./build/examples/orders/ordersWith PORT=0 and no DATABASE_URL, the service does not start: it lists every problem with its error code and exits 1. The same lines go to /dev/termination-log.
$ PORT=0 ./build/examples/orders/orders
docuconf: 2 configuration problems:
PORT: 0 is below min 1 (out_of_range)
DATABASE_URL: is required but not set (missing_required)5.Test your configuration
The example declares its variables in main. To test a declaration, put it in a function that main and the tests both call, as the README does: load(env) reads an explicit map, never the process environment, and throws a ValidationError listing every problem. This is GoogleTest, built against the SDK.
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include <docuconf/docuconf.hpp>
struct Config {
int port = 0;
std::string database_url;
};
// The declaration, in a function that main and the tests both call.
void declare(docuconf::Declaration& d, Config& c) {
d.add_var("PORT", c.port, "HTTP listen port").range(1, 65535).default_val(8080);
d.add_var("DATABASE_URL", c.database_url, "Primary Postgres connection string").secret().required().schemes({"postgres"});
}
TEST(Config, UsesDefaults) {
CLI::App app;
docuconf::Declaration d{app, "orders"};
Config c;
declare(d, c);
// load(env) reads only this map, never the process environment.
d.load({{"DATABASE_URL", "postgres://orders@db/orders"}});
EXPECT_EQ(c.port, 8080);
}
TEST(Config, ReportsEveryProblem) {
CLI::App app;
docuconf::Declaration d{app, "orders"};
Config c;
declare(d, c);
try {
d.load({{"PORT", "70000"}});
FAIL() << "expected a ValidationError";
} catch (const docuconf::ValidationError& e) {
EXPECT_EQ(e.codes_for("PORT"), std::vector<docuconf::Code>{docuconf::Code::OutOfRange});
EXPECT_TRUE(e.has(docuconf::Code::MissingRequired));
}
}cmake --build build --target config_test && ./build/config_test6.Export the contract
The app exports its own contract with --docuconf-export, without reading the environment, so it runs in a Dockerfile RUN step or in CI. --docuconf-export - writes to standard output.
./build/examples/orders/orders --docuconf-export examples/orders/contract.cue// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
generator: {
language: "cpp"
sdk: "docuconf-cpp"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Origins allowed to call the API (CORS)"
default: ["http://localhost:3000"]
items: "string"
encoding: "csv"
separator: ","
minItems: 1
}
DATABASE_URL: {
type: "url"
description: "Primary Postgres connection string"
required: true
secret: true
maxLength: 2048
schemes: ["postgres"]
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level emitted"
default: "info"
values: ["debug", "info", "warn", "error"]
}
PORT: {
type: "int"
description: "HTTP listen port"
default: 8080
min: 1
max: 65535
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Time allowed to read a request"
default: "30s"
min: "1s"
max: "5m"
encoding: "go"
}
WORKER_COUNT: {
type: "int"
description: "Number of request worker threads"
details: "Each worker answers one connection at a time, so this is also the\nnumber of requests served at once. Raise it when requests queue up.\n\nKeep it at or below the database pool size:\n- one connection per worker;\n- plus one for migrations."
default: 4
min: 1
max: 64
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.CLI11 apps
docuconf adds its variables next to your own CLI11 options, and touches the app only for --docuconf-export, the --help listing and the flags you ask for. Variables are read from the environment only; .flag() also accepts one on the command line for local runs (PORT is --port here), with the same checks. --help lists every variable. See the README for file inputs and contract-first mode.
$ ./build/examples/orders/orders --help
orders: a small HTTP service configured with docuconf
Usage: ./build/examples/orders/orders [OPTIONS]
Options:
-h,--help Print this help message and exit
--port INT [8080] HTTP listen port
docuconf:
--docuconf-export PATH Write the configuration contract (contract.cue) to this path, or - for standard output, and exit without reading the environment
Environment variables:
PORT HTTP listen port [int, default "8080", or --port]
LOG_LEVEL Minimum log level emitted [one of debug|info|warn|error, default "info"]
DATABASE_URL Primary Postgres connection string [url, REQUIRED, secret]
ALLOWED_ORIGINS Origins allowed to call the API (CORS) [list of string, separated by ",", default "http://localhost:3000"]
REQUEST_TIMEOUT Time allowed to read a request [duration, default "30s"]
WORKER_COUNT Number of request worker threads [int, default "4"]Next
- The docuconf-cpp README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: No reload: watch. No profiles or config-file overlays. JKS keystores are checked by their integrity digest; their entries are not parsed.
Every snippet on this page is compiled or run against docuconf-cpp's main branch in CI. Facts checked 2026-10-08.