10 Handy Features of C++20
C++20 is usually introduced through its big features: concepts, ranges, coroutines, modules, calendar support, and formatting. Those are important, but they are not the whole story.
Some C++20 changes are smaller and easier to adopt. They do not require a rewrite of your project, and they often fit into code you already have. This article looks at ten practical C++20 features that can make daily C++ code cleaner.
1. Designated Initializers
Designated initializers make aggregate initialization easier to read. Instead of relying only on member order, you can name the members you initialize.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct UartConfig { int baudRate; int dataBits; bool parityEnabled; int stopBits; }; UartConfig debugPort { .baudRate = 115200, .dataBits = 8, .parityEnabled = false, .stopBits = 1 }; |
This is especially useful for configuration structures. The old form works, but it is easier to misread:
|
1 2 3 4 |
UartConfig debugPortOld { 115200, 8, false, 1 }; |
The C++20 version is more explicit. There are some rules: it works for aggregate types, member names must appear in declaration order, and you cannot mix designated and positional initialization in the same initializer.
2. Abbreviated Function Templates
C++20 lets you write simple function templates with auto in the parameter list.
|
1 2 3 4 5 6 |
void print_twice(const auto& value) { std::cout << value << ' ' << value << '\n'; } |
This is equivalent to writing a normal function template:
|
1 2 3 4 5 6 7 |
template <typename T> void print_twice(const T& value) { std::cout << value << ' ' << value << '\n'; } |
For short generic helpers, the abbreviated form keeps the code close to the shape of an ordinary function. It is not always better. If the function is complex or needs several named template parameters, the traditional template syntax can still be clearer.
3. Constrained auto
The same syntax becomes more useful when combined with concepts. You can constrain a parameter directly:
|
1 2 3 4 5 6 7 8 9 |
#include <concepts> double scale_voltage(std::floating_point auto volts, std::floating_point auto gain) { return volts * gain; } |
Now the function accepts floating point types, but rejects integers and unrelated objects.
You can also define your own concept:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <concepts> template <typename T> concept RegisterValue = std::unsigned_integral<T> && (sizeof(T) <= 4); void write_register(RegisterValue auto value) { // send value to a hardware register } |
Before C++20, this kind of constraint often required std::enable_if, tag dispatch, or long static assertions. Concepts put the rule closer to the function interface.
4. Template Syntax for Generic Lambdas
C++14 introduced generic lambdas with auto parameters. C++20 adds explicit template parameter syntax for lambdas.
|
1 2 3 4 5 6 7 8 |
auto print_container = []<typename T>(const std::vector<T>& values) { for (const T& value : values) { std::cout << value << '\n'; } }; |
This helps when you need to refer to the deduced type inside the lambda body. You can also use constraints:
|
1 2 3 4 5 6 7 8 9 10 |
auto sum_integral = []<std::integral T>(std::span<const T> values) { T total {}; for (T value : values) { total += value; } return total; }; |
This is useful in local algorithms, callbacks, and small utilities where a full function template would add too much distance from the code that uses it.
5. consteval for Immediate Functions
constexpr means a function can run at compile time. consteval means it must run at compile time.
|
1 2 3 4 5 6 7 8 |
consteval int make_command_id(char a, char b) { return (static_cast<int>(a) << 8) | static_cast<int>(b); } constexpr int readCommand = make_command_id('R', 'D'); |
A consteval function cannot be called with runtime values:
|
1 2 3 4 5 |
char first = 'W'; // int id = make_command_id(first, 'R'); // error |
That is the point. Use consteval when a helper is only meaningful during compilation, for example small validation functions, generated constants, or safer replacements for macro-like calculations.
6. constinit for Safer Static Initialization
constinit checks that a static or thread local variable is initialized during static initialization. It does not make the variable immutable.
|
1 2 3 4 5 6 7 8 |
constinit int bootCounter = 0; void record_boot() { ++bootCounter; } |
This is different from constexpr, because bootCounter can still change. The useful part is that the compiler rejects dynamic initialization:
|
1 2 3 4 5 6 |
int read_default_port(); // constinit int port = read_default_port(); // error |
For global state, constinit can help avoid static initialization order problems. It is a good fit for simple counters, flags, tables, and configuration values that must be ready before dynamic initialization starts.
7. std::span
std::span is a lightweight view over a contiguous block of elements. It does not own memory. It just carries a pointer and a size.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <span> #include <vector> int average_adc(std::span<const int> samples) { int total = 0; for (int sample : samples) { total += sample; } return samples.empty() ? 0 : total / static_cast<int>(samples.size()); } std::vector<int> buffer { 1010, 1008, 1012 }; int avg = average_adc(buffer); |
The same function can accept an array:
|
1 2 3 4 5 |
int rawSamples[] { 512, 513, 511, 514 }; int rawAvg = average_adc(rawSamples); |
This avoids passing a raw pointer and a separate length. It also avoids copying data. For embedded or performance-sensitive code, std::span is a clean way to express "I need a view of this buffer."
8. starts_with and ends_with
String prefix and suffix checks are common, and C++20 adds direct helpers for them.
|
1 2 3 4 5 6 7 8 |
#include <string> bool is_hex_register_name(const std::string& name) { return name.starts_with("REG_") && name.ends_with("_HEX"); } |
Before C++20, this often involved compare, substr, or manual length checks. The new member functions are simple and readable.
They are available on std::string, std::string_view, and related string types:
|
1 2 3 4 5 6 7 8 |
#include <string_view> bool is_debug_topic(std::string_view topic) { return topic.starts_with("debug/") || topic.ends_with("/trace"); } |
For parsing command names, paths, topics, and identifiers, this is a small but welcome improvement.
9. contains for Associative Containers
In C++17, checking for a key usually looked like this:
|
1 2 3 4 5 6 |
if (settings.find("baud") != settings.end()) { // key exists } |
C++20 adds contains:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <map> #include <string> std::map<std::string, int> settings { {"baud", 115200}, {"timeout_ms", 250} }; if (settings.contains("baud")) { std::cout << "baud rate configured\n"; } |
This works for ordered and unordered associative containers such as std::map, std::set, std::unordered_map, and std::unordered_set.
It does not replace find when you also need the iterator. But for a plain existence check, contains says exactly what the code is doing.
10. std::erase and std::erase_if
Removing elements from standard containers used to require different patterns depending on the container.
For a vector, the classic erase-remove idiom looked like this:
|
1 2 3 4 5 6 7 |
values.erase( std::remove(values.begin(), values.end(), 0), values.end() ); |
C++20 adds std::erase and std::erase_if:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <vector> #include <string> std::vector<int> readings { 10, 0, 12, 0, 13 }; std::erase(readings, 0); std::vector<std::string> names { "adc0", "", "uart1", "" }; std::erase_if(names, [](const std::string& name) { return name.empty(); }); |
The same idea works across many standard containers, with overloads appropriate for each container type. It makes removal code shorter and less error-prone.
A Quick Comparison
Here is one way to think about these features:
| Feature | Helps with |
|---|---|
| Designated initializers | More readable aggregate setup |
| Abbreviated templates | Short generic helpers |
Constrained auto |
Clearer type requirements |
| Template lambdas | Local generic code |
consteval |
Compile time only helpers |
constinit |
Safer static initialization |
std::span |
Non-owning buffer views |
starts_with and ends_with |
Cleaner string checks |
contains |
Direct key existence checks |
std::erase_if |
Simpler container cleanup |
Conclusion
C++20 is not only about large language features. It also improves many small pieces of everyday code.
Some features make intent clearer, like designated initializers, contains, and string prefix checks. Some reduce template noise, like constrained auto and template lambdas. Others tighten correctness, like consteval, constinit, and std::span.
You do not need to adopt all of C++20 at once. Start with the features that remove real friction in your codebase. These ten are good candidates because they are practical, readable, and easy to introduce gradually.