Step 7 of 7
Attributes, consteval and constinit
Modern C++ adds small annotations that tell the compiler what you mean, so it can catch mistakes or optimize, without changing what correct code does.
Attributes
Attributes go in double square brackets:
| Attribute | Meaning |
|---|---|
[[nodiscard]] |
warn if a caller ignores the return value (an error code, a newly allocated object) |
[[maybe_unused]] |
don't warn that this variable or parameter is unused |
[[fallthrough]] |
this case deliberately continues into the next one |
[[deprecated("use x")]] |
warn anyone who still calls this |
[[likely]] / [[unlikely]] (C++20) |
a hint about which branch usually runs |
[[nodiscard]] is the one worth adding everywhere it applies. [[nodiscard]] bool save(const Config&); turns save(cfg); (forgetting to check) into a compiler warning.
constexpr, consteval, constinit
constexprfunction: may run at compile time, when its arguments are constants and the result is needed as a constant, and runs at run time otherwise.constevalfunction (C++20): must run at compile time. Calling it with a value only known at run time is a compile error. Use it when a run-time call would be a mistake, like hashing a fixed string or validating a format.constinitvariable (C++20): must be initialized at compile time, but can change later. It's for globals: the variable is ready before any code runs, so it can't be caught by the "static initialization order fiasco" (one global's constructor reading another global that isn't set up yet). If its initializer can't be computed at compile time, the compiler says so instead of silently deferring it to run time.
#include <iostream>
consteval int kib(int n) { return n * 1024; } // compile time only
constexpr int square(int n) { return n * n; } // either
constinit int buffer_size = kib(4); // set before main runs; still changeable
[[nodiscard]] bool resize(int& size, int wanted) {
if (wanted <= 0) return false;
size = wanted;
return true;
}
int main() {
static_assert(kib(2) == 2048);
int n = 5;
std::cout << square(n) << " " << buffer_size << "\n"; // square at run time here
if (!resize(buffer_size, kib(8))) return 1;
std::cout << buffer_size << "\n";
}
25 4096
8192
Calling kib(n) with the run-time variable n would not compile, and writing resize(buffer_size, 100); without checking the result gets a warning.
The classic use: switching on strings
A switch only works on integers (and enums), and each case needs a compile-time constant. A consteval hash turns fixed strings into constants, and a constexpr version of the same hash handles the run-time input.
Your turn: write fnv1a(s) as a constexpr 32-bit FNV-1a hash (start from 2166136261; for each character, XOR in the character as an unsigned char, then multiply by 16777619), id_of(s) as a consteval wrapper around it, and command_code(cmd), which switches on fnv1a(cmd) with case id_of("start"), id_of("stop") and id_of("pause") returning 1, 2 and 3, and -1 for anything else. Mark command_code [[nodiscard]].