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 task
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]].
Challenge 2
Write consteval int square(int n), then a constant kLimit equal to square(64), and [[nodiscard]] bool fits(int bytes) that is true when bytes <= kLimit.
Challenge 3
Grades stack up: 'S' gets 50 plus everything 'A' gets, 'A' gets 30 plus everything 'B' gets, and 'B' gets 20. Anything else gets 0. Write int bonus(char grade) as a switch where each case falls through to the next, marked with [[fallthrough]]. The compiler warns about an unmarked fall-through, and warnings are errors here.