Use this guide whenever writing or reviewing C++ syntax. Canonical rules:
SYN-* and NAM-* in AGENTS.md.
The purpose of a style rule is to make ownership, type, control flow, and intent immediately legible to both humans and agents. Do not trade clarity for novelty.
| Entity | Required shape | Correct | Incorrect |
|---|---|---|---|
| Type, class, struct, concept | PascalCase | ExpressionParser |
expression_parser |
enum class enumerator |
PascalCase | EmptyExpression |
empty_expression |
| Function | lowerCamelCase | parseExpression |
parse_expression |
| Parameter or local | lowerCamelCase | expressionText |
expression_text |
| Private/protected member | m_ + lowerCamelCase |
m_expressionText |
my_member, mymember_ |
| Boolean | lowerCamelCase predicate | isReady, hasError |
readyFlag, error_bool |
| Constant | lowerCamelCase with type guarantees | maximumDepth |
MAX_DEPTH |
| QML type/file | PascalCase | PrimaryActionButton.qml |
primary_action_button.qml |
| QML id/property/signal | lowerCamelCase | resultDisplay |
result_display |
Use scoped enumerations and PascalCase enumerators.
Correct
enum class ErrorCode {
EmptyExpression,
InvalidToken,
DivisionByZero
};
[[nodiscard]] std::string_view describeError(ErrorCode errorCode)
{
switch (errorCode) {
case ErrorCode::EmptyExpression:
return "Expression must not be empty.";
case ErrorCode::InvalidToken:
return "Expression contains an invalid token.";
case ErrorCode::DivisionByZero:
return "Division by zero is not allowed.";
}
std::unreachable();
}Incorrect
enum ErrorCode {
empty_expression,
invalid_token
};
case ErrorCode::empty_expression:The incorrect form violates NAM-003, SYN-004, and strong type scoping.
Correct
class ExpressionEvaluator final {
public:
explicit ExpressionEvaluator(Precision precision);
[[nodiscard]] auto evaluate(std::string_view expressionText) const
-> std::expected<double, ErrorCode>;
private:
Precision m_precision;
bool m_isTracingEnabled {false};
};Incorrect
class expression_evaluator {
public:
expression_evaluator(Precision precision);
double evaluate(std::string expression_text);
private:
Precision my_member;
bool tracing_enabled_;
};Private members are always m_ plus lowerCamelCase. A trailing underscore does
not become acceptable because another framework or style guide uses it.
Return syntax is selected for readability, not enforced mechanically. Use a leading return type when it is short and immediately understandable. Use a trailing return type when the language requires it or when it makes a complex, dependent, or multiline declaration easier to scan.
Correct
void inputDecimalPoint();
[[nodiscard]] bool isReady() const noexcept;
[[nodiscard]] auto loadExpression(std::filesystem::path path)
-> std::expected<Expression, LoadError>;The first two signatures are clearer without auto ... -> void or
auto ... -> bool. The final signature benefits from aligning a longer result
contract on its own line. Do not rewrite between the two forms without a
readability or language reason.
Incorrect for naming and failure modeling
Expression load_expression(std::filesystem::path path);The incorrect form hides the failure contract and violates the naming rules; its leading return type is not the problem.
For new project-owned console output, use C++23 formatted output directly.
std::print is appropriate when no newline is wanted, and std::println is the
default for complete lines.
Correct
std::println("Processed {} records in {} ms", recordCount, elapsed.count());
std::print("Progress: {}%\r", percentage);Incorrect for ordinary project output
std::cout << "Processed " << recordCount << " records\n";
std::cerr << "Operation failed: " << error.message() << '\n';Use std::format when formatted text must be stored or passed to another API.
A third-party or legacy boundary that accepts only std::ostream may use
stream insertion locally, but the boundary and reason must be explicit. Do not
use C globals such as stdout or stderr when a standard C++ output facility
expresses the operation.
- Initialize every variable at declaration.
- Prefer
constwhen a local does not change. - Give data members safe default state when meaningful.
- Use braces when they prevent narrowing or clarify construction.
- Do not use a default value to disguise an error.
const auto tokenCount = tokens.size();
auto result = std::optional<Result> {};
auto retryCount = std::size_t {0};Use nullptr. Use named casts only when the conversion is intentional and
locally understandable.
const auto itemCount = static_cast<std::size_t>(validatedCount);
auto* target = dynamic_cast<Target*>(baseObject);reinterpret_cast requires a low-level boundary and explanation. C-style casts
are never a shortcut around type design.
- Always use braces.
- Prefer early returns when they reduce nesting.
- Keep conditions named when their meaning is not obvious.
- Do not combine unrelated mutation and decisions in one expression.
- Use exhaustive
switchhandling for closed alternatives.
Correct
if (expressionText.empty()) {
return std::unexpected{ErrorCode::EmptyExpression};
}Incorrect
if (expression_text.empty()) return false;Do not place using namespace at namespace scope. Prefer qualified names or a
narrow alias close to its use.
namespace ranges = std::ranges;
Iterator findRule(std::span<const Rule> rules)
{
return ranges::find_if(rules, predicate);
}Use using for project-owned aliases:
using ParseResult = std::expected<Expression, ErrorCode>;Do not introduce new typedef declarations.
Order a class for contract-first reading:
public constructors and special members
public operations
protected extension points, only when intentional
private operations
private data
Invariant-bearing data remains private. A struct may expose data only when it
is deliberately a transparent passive value without a hidden invariant.
Constructor initializer lists follow declaration order.
Order a module implementation for predictable reading:
module declaration
imports
namespace
member/public definitions
private local helpers
Use the narrowest capture list that explains ownership. A lambda that escapes
the current call must not casually use [&].
auto task = [request = std::move(request), owner = m_owner]() mutable {
return owner->execute(std::move(request));
};Before storing or dispatching a lambda, verify the lifetime of every captured pointer, reference, Qt object, and cancellation token.
Use auto when the initializer communicates the type or repeating the type
adds noise. Spell out a domain type when it makes ownership, precision, or a
conversion materially clearer. Prefer range-based loops and stable range
algorithms, but do not force them when an indexed algorithm is clearer.
Macros are limited to unavoidable preprocessing and external compatibility. Use language constructs for project-owned constants and behavior.
inline constexpr auto maximumDepth = std::size_t {64};Do not write:
#define MAX_DEPTH 64
#define RETURN_IF_ERROR(value) /* hidden control flow */- Check module and namespace identity.
- Check every exported and project-owned identifier.
- Check enum declarations and every
caselabel. - Check private/protected data member prefixes.
- Check initialization, constness, casts, nullability, and control-flow braces.
- Check result/error contracts and
[[nodiscard]]. - Check that return syntax is deliberate rather than mechanically uniform.
- Reject new iostream insertion for ordinary formatted console output.
- Reject unrelated formatting churn during functional changes.