Skip to Content
Course content

6: Understanding C++ Standards: C++11 through C++23

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was onboarding a talented dev who had spent most of his time in Java. He was diving into a legacy codebase for us—a high-frequency trading tool written back in the early 2000s—but he was reading the most recent C++ documentation online. He spent an entire afternoon trying to use std::filesystem to organize log directories, but the compiler kept throwing a fit, claiming the namespace didn't exist. He thought the library was broken. In reality, the project was still being compiled under the C++98 standard. He was trying to use a 2017 feature in a 1998 world.

That's the thing about C++: it isn't just one language. It's a series of evolving standards. If you don't know which "version" of C++ you're writing, you'll either miss out on incredibly powerful tools or spend hours debugging syntax that your compiler doesn't even recognize yet. I always tell people that C++ is split into two eras: "Classic C++" (everything before 2011) and "Modern C++" (C++11 and beyond).

The C++11 Watershed and the Move to Modernity

If you're looking at a piece of code and see auto, lambda expressions, or nullptr, you're looking at C++11 or newer. Before C++11, the language felt clunky. We had to be painfully explicit about types, and managing memory was a nightmare of raw pointers and manual delete calls. C++11 changed the game by introducing move semantics. I remember when std::move first hit; it felt like we finally stopped fighting the language over expensive copies of large objects. Instead of copying a massive vector, we could finally just "steal" the resources from a temporary object.

// The old way: potentially expensive copies
std::vector<int> processData(std::vector<int> input) { 
    // ... 
}

// The modern way: move semantics (C++11)
std::vector<int> processData(std::vector<int>>&& input) {
    // We move the data instead of copying it
}


Refining the Experience with C++14 and C++17

After the earthquake that was C++11, the committee moved to a predictable three-year release cycle. C++14 was mostly a "polish" release, fixing small annoyances. But C++17 is where things got really interesting for me. It introduced structured bindings, which let us unpack tuples or structs in a single line. It also gave us std::optional, which is a godsend for functions that might not actually return a value. I can't tell you how many "magic values" (like returning -1 to indicate an error) I've been able to scrub from my code thanks to std::optional.

// C++17 Structured Bindings
auto [it, inserted] = my_map.insert({"key", 42});
if (inserted) {
    // We know it worked without manually checking the iterator
}


The Heavy Hitters: C++20 and C++23

Now we're in the era of "Big Features." C++20 is probably the most significant update since C++11. It introduced Concepts, which basically allow us to put constraints on template parameters. No more staring at ten pages of incomprehensible template error messages because you passed an int to something that needed a FloatingPoint type. We also got Ranges, which let us pipe operations together—similar to how you'd see in Rust or Python—and Coroutines for asynchronous programming.

C++23 is more of a refinement, similar to C++14, but it adds things like std::expected for better error handling and std::print, which finally gives us a modern, fast way to output text without the clunkiness of std::cout. When you're starting a new project today, I highly recommend targeting at least C++20 if your compiler supports it. The productivity gain from Concepts and Ranges alone is massive.




📋 Practical Task

Exercise: Modernizing a Legacy Data Processor

You have been handed a snippet of "Classic C++" code used to filter a list of integers. Your task is to refactor this code to use C++17 and C++20 features. Specifically, you need to replace the manual iterator loop with a Range-based for loop or std::ranges, replace the raw pointer/manual memory management with a smart pointer or standard container, and use auto to simplify the type declarations.

// LEGACY CODE TO FIX
#include <iostream>
#include <vector>
#include <algorithm>

void filterEvenNumbers(int* arr, size_t size) {
    std::vector<int> evens;
    for (std::vector<int>::iterator it = evens.begin(); it != evens.end(); ++it) {
        // This loop is actually broken/empty in the legacy code!
    }
    // Imagine a manual loop here that pushes back evens...
}

int main() {
    int* data = new int[5]{1, 2, 3, 4, 5};
    filterEvenNumbers(data, 5);
    delete[] data; 
    return 0;
}

Requirements for your solution:

  • Eliminate the new[] and delete[] calls entirely.
  • Use std::vector and auto for all iterations.
  • (Bonus) Use std::ranges::filter_view or a simple range-based for loop to identify the even numbers.
  • Ensure the final code compiles under the -std=c++20 flag.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.