- Class names use CamelCase. Method, function, and variable names use snake_case.
- Class data members have an
m_prefix and are usually private. Struct data members have no prefix and are usually public. - Use
void set_attribute(int value)andint attribute() constfor setters and getters; avoid aget_prefix. Follow the Qt recommendations for boolean getters. - Structs are usually small and simple, have few or no methods, and never use inheritance.
- Files use CamelCase when they contain a CamelCase class. Otherwise, files use snake_case and their contents belong to a matching snake_case namespace.
- Reflect the directory and file structure in namespaces. For example,
folder/structure.hcorresponds tonamespace folder::structure { ... }. - Indent with four spaces. Do not use tabs.
- Format new code, and only new code, with the repository's
.clang-formatfile. Do not reformat unrelated existing code. - Follow the Qt API design principles and the C++ Core Guidelines where this guide does not specify a rule.
Avoid repeating the same name across namespaces and types. For example, io::envelope::Envelope is undesirable. A supporting type such as io::envelope::Version reads well, but placing the main class in that namespace forces its longer, repetitive name.
Prefer io::Envelope. Put supporting types either inside io::Envelope as public types or in an io::envelope_details namespace.
Avoid out parameters in new APIs when practical; prefer returning a small struct. When an out parameter is necessary, use a pointer rather than a non-const reference. The address-of operator at the call site makes it clear that the argument can be modified:
color.get_hsv(&hue, &saturation, &value);Names are a central part of API design. Avoid abbreviations, even familiar ones, because consistent full words are easier to understand and remember. Choose precise names that preserve useful context without adding redundant words. If a concept is difficult to name or describe in one sentence, reconsider whether it belongs in the API.
Use consistent names for related classes and operations. A function name should make clear whether the function has side effects. Give parameters descriptive names and use the same names in declarations, definitions, and documentation. Avoid repeating an enum type's name in scoped enumerators when the scope already provides the context.
Failures fall into three tiers:
- Bugs: violated invariants and preconditions, including results that can fail only through a programming error. Use libassert
ASSERT,DEBUG_ASSERTfor checks too expensive for release builds,PANICorUNREACHABLE, andError::asserting_unwrapfor such anExpected. They abort with a stack trace. - Recoverable failures: reusable functions, including all of terrainlib, return
Expected<T>, which is an alias forstd::expected<T, Error>, whenever a caller might react to the failure. Library code does not throw. - Unrecoverable failures in tools: application code of command-line tools may throw
Error::Exceptionfor invalid input and runtime failures it cannot recover from, usingError::raisefor new errors andError::throwing_unwrapfor failed results. The tool'smaincatches it, logs the error and its stack trace, and exits with a nonzero status.
Every Error records the stack trace where it was made; Error::stacktrace() resolves it for logging.
The rest of this section describes Expected results.
Use the short boolean form in control flow:
if (!result) {
// Handle error.
}
if (result) {
// Handle value.
}Use Error::fail where an error originates instead of std::unexpected:
return Error::fail(
Error::Code::InvalidInput,
"invalid storage layout");Choose the most specific error code available.
Generally, errors can be forwarded with Error::propagate:
auto result = operation(); // Expected<Source>
if (!result) {
// result can be either a std::expected or an Error.
return Error::propagate(std::move(result));
}
// The current function returns Expected<Target>.propagate adds the source file and line as additional context. In some cases, it can be useful to avoid that:
auto result = operation();
if (!result) {
return result;
}This is appropriate for transparent helpers, recursive calls, and simple delegation where another error frame would add no useful information. When the success types differ and direct return is impossible, use propagate.
At meaningful abstraction or operation boundaries, propagate with a concise context message:
auto result = write_payload(path);
if (!result) {
// result can be either a std::expected or an Error.
return Error::propagate(std::move(result), "write DAG metadata");
}Useful context identifies what the caller was trying to accomplish, such as:
- The domain operation:
"gather relevant input leaves". - The data role:
"read DAG clustering". - The affected node:
"add saved node " + key + " to storage index". - The higher-level phase:
"finalize merged output dataset".
Avoid merely repeating details already supplied by the lower-level error. For example, if the lower layer records the exact path and I/O operation, the caller should add the file's semantic role rather than repeat the path unnecessarily.
The error type can be changed (reclassified) as well:
auto result = write_payload(path);
if (!result) {
return Error::propagate(
std::move(result),
Error::Code::CorruptData,
"write DAG metadata");
}Reclassify an existing error only if the current abstraction genuinely changes its meaning.