diff options
| author | Leitwolf11 | 2026-10-18 20:00:39 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-18 20:00:39 +0300 |
| commit | 8d1fb4807edc7920fbecd0554c9c61625827e19c (patch) | |
| tree | dd3d7e1d4f48ca27140a2bd5f970817ad02ae35a /Guide/LANGUAGE.md | |
| download | progmasoft-cpp-8d1fb4807edc7920fbecd0554c9c61625827e19c.tar.gz | |
Write the first revision of Progmasoft C++ Style
Progmasoft C++ Style is a style guide for C++20, written for anyone
who wants a complete rule set to adopt. It writes down the conventions
the Progmasoft C++ projects already follow (naming, the clang-format
and clang-tidy profiles, exception-free error handling, Doxygen) and
decides what they left open.
Thirteen chapters under Guide/:
1. Principles: what the guide optimizes for and how to decide a case
it does not cover.
2. Formatting: the full .clang-format profile, the reason for each
choice, and the rules the tool cannot apply.
3. Naming: casing for every kind of name, acronyms, choosing words,
namespaces, files.
4. Files and headers: extensions, #pragma once, includes, what goes in
a header, the order inside a class, file size, vendored code.
5. Language: functions, auto, initialization, const and constexpr,
casts, integers, strings, enumerations, templates and concepts,
lambdas, control flow, and the features not used.
6. Types and classes: struct or class, final, construction, the rule of
zero, inheritance, operators, strong types, optional and variant.
7. Errors: no exceptions by default, Expected and Error, invariants,
fatal defects, destructors, diagnostics.
8. Ownership and memory: owners and borrowers, views and lifetimes,
RAII, containers and deterministic iteration, allocation, undefined
behavior.
9. Comments and documentation: Doxygen for public declarations,
comments that explain why, TODOs that name an issue.
10. C interfaces: C11 headers, prefixes, opaque types, ownership and
failure across the boundary, stability.
11. Concurrency: sharing less, threads, locks, thread-safety
annotations, atomics, statics.
12. Testing: unit tests, fuzzing, sanitizers, benchmarks, coverage.
13. Tooling: the compiler flags, the full .clang-tidy profile with the
reason for every disabled check, other checks, version pinning.
The guide prescribes no test, fuzzing or benchmark framework and no
error library; it prescribes how they are used. ADOPTING.md explains
how a project points to the guide, copies the profiles and records its
deviations. CONTRIBUTING.md explains how to propose a change.
The text is licensed under CC BY 4.0. The configuration and code
examples are dedicated to the public domain under CC0 1.0, so projects
can copy them under their own license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'Guide/LANGUAGE.md')
| -rw-r--r-- | Guide/LANGUAGE.md | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/Guide/LANGUAGE.md b/Guide/LANGUAGE.md new file mode 100644 index 0000000..7163472 --- /dev/null +++ b/Guide/LANGUAGE.md @@ -0,0 +1,170 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: CC-BY-4.0 --> + +# 5. Language + +The guide targets C++20 exactly: `-std=c++20`, no compiler extensions (`-Wpedantic` is on). A C++23 feature is used only +through a library that provides it for C++20, such as `llvm::Expected` in place of `std::expected`. + +## 5.1 Functions + +- **Trailing return types for functions that return a value**: `auto Size() const -> std::size_t`. Return types then + line up, and a long return type does not push the name to the right. A function that returns nothing is declared + `void`, which reads better than `auto … -> void`. +- **`[[nodiscard]]` on every function whose result matters**, and always on one that returns `Expected`, `Error`, an + owning pointer or a status. Ignoring such a result is then a compile error under `-Werror`. +- **Parameters** take an input by value when the function keeps a copy or the type is cheap to copy (scalars, + `std::string_view`, `std::span`, small trivially copyable structs); by `const &` otherwise; by `&` only for an output + or an in-out parameter, and prefer returning a value over an output parameter. +- **Default arguments** are allowed on non-virtual functions and constructors. A virtual function does not take them, + because the default would depend on the static type of the call. +- **Overloads** do the same thing for different types. When two functions do different things, give them different + names. +- **No function-style "flag" parameters** that select behavior: `Emit(module, true)` is unreadable at the call. Use an + `enum class` or two functions. +- **`noexcept`** goes on move constructors, move assignment, `swap`, destructors (implicitly) and functions that by + contract cannot fail. In exception-free code it documents that a function has no failure path; do not add it to + everything. + +## 5.2 `auto` + +Use `auto` when the type is obvious from the right side, or long and uninformative: + +```cpp +// The type is on the right. +auto parser = Parser::Create(source); +// The iterator type adds nothing. +auto iterator = blocks.find(id); +// A structured binding. +const auto &[name, value] = *entry; +``` + +Write the type when it carries information the reader needs, especially for numbers: + +```cpp +// The width and the signedness matter. +std::uint32_t offset = header.Offset(); +std::size_t count = Count(items); +``` + +Never `auto` for a variable that is meant to be a different type from the initializer, such as a proxy object +(`std::vector<bool>::reference`) or an expression template. + +## 5.3 Initialization + +- **Initialize every variable where it is declared.** An uninitialized variable is a bug waiting for a path that reads + it. Declare a variable at the point where it gets its value, in the smallest scope. +- **Brace-initialize data members** with a default: `std::uint32_t count_{};`, `bool available{};`. Every object then + starts in a defined state, and `-Wmissing-field-initializers` stays quiet for aggregate initialization that omits + trailing fields. +- **Braces for initialization, parentheses for a constructor that braces would hide.** `std::vector<int> values(3, 0)` + makes three zeros; `std::vector<int> values{3, 0}` makes the list `{3, 0}`. Use parentheses only where braces would + select an `std::initializer_list` constructor you do not want. +- **Designated initializers** name the fields of an aggregate: `Edge{.from = a, .to = b}`. Use them whenever an aggregate + has more than two fields or two fields of the same type. +- **No narrowing.** Brace initialization refuses it; with `=` and parentheses, write the conversion out. + +## 5.4 `const` and `constexpr` + +- **`const` by default.** A local that does not change is `const`; a member function that does not change the object is + `const`; a pointer or reference parameter that is only read points to `const`. +- **`constexpr` for every value known at compile time**, and for functions that can be evaluated at compile time without + contortion. `consteval` when a function must only run at compile time. +- **`inline constexpr`** for a constant in a header. `constexpr` on a static data member already implies `inline`. +- Do not `const`-qualify a by-value parameter in a declaration; it means nothing to the caller. It may be `const` in the + definition. +- **No `const_cast`** to write through a pointer that refers to a `const` object. Removing `const` is allowed only to call + a C API that takes a non-`const` pointer it is documented not to modify, with a comment saying so. + +## 5.5 Casts and conversions + +- **No C-style casts and no functional casts** for anything but constructing a value of class type. Use + `static_cast` for a value conversion, `std::bit_cast` to reinterpret the bits of a trivially copyable value, and + `reinterpret_cast` only at a byte or ABI boundary, with a comment saying why the cast is valid. +- **No implicit conversion between integer widths or signedness.** `-Wimplicit-int-conversion` and the clang-tidy + profile reject them; write the `static_cast` and make sure it cannot lose a value, or check that it cannot. +- **No implicit conversion to `bool`.** Write `pointer != nullptr`, `count != 0U`, `!text.empty()`. +- **`explicit`** on every constructor that can be called with one argument and on every conversion operator, unless the + implicit conversion is the point of the type, as it is for a view. + +## 5.6 Integers and arithmetic + +- **Sizes and indices into memory are `std::size_t`.** Counts that come from or go to a file, a protocol or an ABI use + the fixed-width types of `<cstdint>`, such as `std::uint32_t`. Do not use `long`, `unsigned long` or `short`, whose + widths differ between platforms. +- **Unsigned for bits and sizes, signed for arithmetic that can go below zero.** Do not use unsigned to say "never + negative" for a value that is computed by subtraction. +- **Integer literals carry their type**: `0U`, `1ULL << 40U`, `std::size_t{0}`. +- **Overflow of an untrusted value is checked**, before the operation, or with `__builtin_add_overflow` and its + siblings, or with a checked-arithmetic helper. Signed overflow is undefined behavior; unsigned wrap-around is defined + but almost never what a size computation means. +- **Floating point** compares with a tolerance that the code names, never with `==` on computed values. + +## 5.7 Strings and text + +- Text is UTF-8 in `std::string`. Take a read-only string parameter as `std::string_view` (or `llvm::StringRef` in code + built on LLVM). Do not use `std::wstring`, `wchar_t`, or `std::u8string` in an interface. +- Convert to a platform encoding only at the platform boundary, such as a Windows wide-character API. +- Format with `std::format`-style or `llvm::formatv`-style functions, or by streaming into a string builder; never with + `sprintf` into a fixed buffer. +- A view parameter must not be stored. A member that keeps text owns a `std::string`. + +## 5.8 Enumerations + +Every enumeration is an `enum class`. Give it an explicit underlying type when its values are stored, sent or matched +against an ABI: + +```cpp +enum class Opcode : std::uint8_t +{ + kLoad = 1, + kStore = 2, +}; +``` + +A `switch` over an enumeration lists every enumerator and has no `default`, so that `-Wswitch` reports a case that a new +enumerator needs. When a value may come from outside the program, convert it to the enumeration with a function that +validates it. + +## 5.9 Templates and concepts + +- **Constrain every template** with a concept or a `requires` clause instead of SFINAE: `template<std::integral Id>`. + The error then names the requirement, not a substitution failure deep inside. +- **Name a concept for a requirement used more than once.** Concepts are `PascalCase` nouns or adjectives: + `Hashable`, `ByteRange`. +- **Keep templates thin.** Move the part that does not depend on the template parameters into a non-template function, + to keep compile times and code size down. +- **No template metaprogramming where `constexpr` code would do.** An `if constexpr` branch or a `consteval` function is + easier to read than a recursive specialization. + +## 5.10 Lambdas + +- **Capture explicitly.** `[&]` is allowed for a lambda that does not outlive the statement it is written in, such as a + callback passed to an algorithm. A lambda that is stored, returned or run later names each capture, and captures by + value whatever it must keep. +- Never capture `this` implicitly. Write `[this]` so the lifetime dependency is visible. +- A lambda longer than a few lines, or used twice, becomes a named function. + +## 5.11 Control flow + +- **Return early** for errors and edge cases; keep the main path unindented. +- **No `goto`.** +- **Range-based `for`** whenever the index is not needed. When it is, iterate with `std::size_t`. +- **`[[fallthrough]]`** marks every intended fall-through in a `switch`; `-Wimplicit-fallthrough` rejects the rest. +- **Ranges and algorithms** where they state the intent better than a loop: `std::ranges::sort(items)`, + `std::ranges::any_of(...)`. A plain loop is better than a pipeline of five views that needs a comment to explain. +- **`[[likely]]` and `[[unlikely]]`** only where a measurement showed a difference. + +## 5.12 Features not used + +| Feature | Instead | +| --- | --- | +| C++20 modules | headers; build-tool support is not yet uniform across the supported compilers | +| Coroutines | explicit state machines or callbacks, until a project adopts a coroutine library deliberately | +| RTTI: `dynamic_cast`, `typeid` | `std::variant` and `std::visit`, a kind enumeration, or LLVM-style `isa`/`cast`/`dyn_cast` | +| Exceptions | see [Errors](ERRORS.md) | +| `std::bind` | a lambda | +| Variable-length arrays | `std::array`, `std::vector`, or a small-vector type | +| Non-`const` global and static variables | state passed explicitly, owned by an object with a clear lifetime | +| `alloca`, `setjmp`/`longjmp` | ordinary allocation and returns | +| User-defined literals | named functions or constructors, except the standard ones such as `std::chrono` suffixes | |