Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/Guide/LANGUAGE.md
blob: 7163472bc2e2cd29fc218719e6677b3d355db99a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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 |