C++ Versions Explained: From C++98 to C++23
C++ has changed a lot since its first ISO standard. Each version adds language and library features, while older code usually remains valid. The current published standard is C++23; C++26 is still in progress. Standard C++
The major versions at a glance
| Version | What it brought | Example |
|---|---|---|
| C++98/03 | The original standardized C++: classes, templates, STL containers and algorithms | std::vector<int> |
| C++11 | A major modernization: auto, lambdas, move semantics, smart pointers, and threads | auto n = 42; |
| C++14 | Refinements to C++11, including generic lambdas and more flexible constexpr | [](auto x) { return x * 2; } |
| C++17 | Structured bindings, if constexpr, std::optional, and filesystem support | if (auto it = map.find(key); it != map.end()) |
| C++20 | Concepts, ranges, coroutines, modules, and std::span | template<std::integral T> |
| C++23 | Improvements to C++20, including std::expected, std::print, and more library support | std::expected<int, Error> |
The changes you’ll notice in code
C++98 to C++11: safer, shorter everyday code
Before C++11, code often spelled out types and managed ownership manually:
std::vector<int>::const_iterator it = values.begin();
C++11 added auto and range-based loops:
for (auto value : values) {
std::cout << value << '\n';
}
It also introduced std::unique_ptr, which makes single-owner dynamic memory much easier to manage safely.
C++17: convenient modern building blocks
Structured bindings let you unpack pairs and tuples directly:
auto [position, inserted] = names.emplace("Ada", 1);
std::optional represents a value that may be absent without using a special sentinel:
std::optional<int> find_id(std::string_view name);
C++20: express requirements and data pipelines
Concepts let a template state what kinds of types it accepts:
template<std::integral T>
T twice(T value) {
return value * 2;
}
Ranges make common collection processing more composable, and coroutines provide language support for suspendable functions. These features can improve larger codebases, but they may take time to learn.
C++23: useful additions, with compiler support to check
std::expected can return either a result or an error explicitly:
std::expected<int, ParseError> parse_number(std::string_view text);
C++23 also adds std::print for convenient formatted output. Availability depends on the compiler and its standard library version.
Which version should you use?
- Starting a new project? Choose the newest standard supported by your toolchain and deployment targets. C++20 is a practical baseline for many projects; use C++23 when your compiler and libraries support the features you need.
- Maintaining an existing project? Keep its configured standard unless you have a reason to upgrade. A newer compiler alone does not automatically change the language version.
- Building for embedded or older systems? Check the exact compiler and library support on the target. You can often use newer features selectively, but portability depends on the whole toolchain.
Set the standard explicitly, for example with -std=c++20 or -std=c++23 in GCC or Clang. Check compiler and standard library support before relying on a particular feature; support can vary by feature and release.
The C++ version tells you which language and library features are standardized. It does not guarantee that every compiler implements every feature equally well.