← Back to BlogArticle

Modern C++20/23: Best Practices for 2026

C++ continues evolving with C++20, C++23, and upcoming C++26. This guide covers the modern features that will make your code safer, faster, and more maintainable in 2026.

Smart Pointers: The Foundation of Safe Memory

Replace raw new/delete with smart pointers. It's the single biggest improvement you can make:

// Instead of: T* p = new T;
#include <memory>

// Use unique_ptr for exclusive ownership
auto ptr = std::make_unique<MyClass>();

// Use shared_ptr for shared ownership
auto shared = std::make_shared<MyClass>();

// shared_ptr respects polymorphism
std::shared_ptr<Base> base = std::make_shared<Derived>();

C++20 Concepts: Constrain Your Templates

Concepts replace SFINAE with readable constraints:

#include <concepts>

template<typename T>
concept Addable = requires(T a, T b) {
  a + b;
};

template<Addable T>
T add(T a, T b) {
  return a + b;
}

Ranges: Replace Iterators with Views

C++20 ranges make algorithms composable and readable:

#include <ranges>
#include <vector>
#include <algorithm>

std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};

// Old way
std::vector<int> result;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result),
  [](int n) { return n % 2 == 0; });

// C++20 way
auto result = nums | std::views::filter([](int n) { return n % 2 == 0; })
                 | std::views::transform([](int n) { return n * 2; });

// Chain: filter, then transform
auto filtered = nums | std::views::filter([](int n) { return n > 3; });

C++20 Modules:Beyond #include

Modules replace header files for faster builds:

// math module (math.ixx)
export module math;

export int add(int a, int b) {
  return a + b;
}

// main.cpp
import math;

int main() {
  return add(1, 2); // 3
}

C++23 std::expected: Error Handling

Return errors explicitly instead of exceptions:

#include <expected>

std::expected<int, std::string> divide(int a, int b) {
  if (b == 0) {
    return std::unexpected{"Cannot divide by zero"};
  }
  return a / b;
}

// Usage
auto result = divide(10, 2);
if (result) {
  std::cout << *result << "\\n";  // 5
} else {
  std::cerr << result.error() << "\\n";  // Cannot divide by zero
}

Best Practices Summary

  • Always use smart pointers, never raw new/delete
  • Use concepts instead of SFINAE
  • Replace std::copy_if with ranges | views
  • Use std::expected for error handling
  • Enable sanitizers in CI: -fsanitize=address,undefined