Cpp Notes

ch12_cpp20_core_features

C++ 20 Core Features

Working with modules

-- maybe later

Understanding module partitions

-- maybe later

Using requires expressions and clauses

  • Purpose of Concepts in C++20
    • Simplify and clarify template constraints
    • Replace verbose and error-prone std::enable_if patterns
    • Improve code readability, diagnostics, and intent expression

  • Traditional Constraint with std::enable_if (C++11)
    template <typename T,
              typename = std::enable_if_t<std::is_arithmetic_v<T>, T>>
    struct NumericalValue {
      T value;
    };
    • Only allows instantiating with arithmetic types (e.g., int, double)
    • Example usage:
      auto nv = wrap(42);           // OK
      auto ns = wrap("42"s);        // Error

  • Defining and Using Concepts (C++20)

    • Simple Concept using type traits:
      template <class T>
      concept Numerical = std::is_arithmetic_v<T>;
    • Using Standard Concepts from <concepts>:
      template <class T>
      concept Numerical = std::integral<T> || std::floating_point<T>;
  • Applying Concepts to Templates

    • Replaces typename/class with concept name:

      template <Numerical T>
      struct NumericalValue {
        T value;
      };
      
      template <Numerical T>
      NumericalValue<T> wrap(T value) { return { value }; }
      
      template <Numerical T>
      T unwrap(NumericalValue<T> t) { return t.value; }
  • Usage

    auto nv = wrap(42);            // OK
    std::cout << nv.value << '\n'; // 42
    auto v = unwrap(nv);          // OK
    std::cout << v << '\n';       // 42
    
    using namespace std::string_literals;
    auto ns = wrap("42"s);        // Error (not Numerical)

  • Concepts as Constraints on Variable Templates

    template <class T>
    concept Real = std::is_floating_point_v<T>;
    
    template <Real T>
    constexpr T pi = T(3.1415926535897932385L);
    
    std::cout << pi<double> << '\n'; // OK
    std::cout << pi<int> << '\n';    // Error

  • Composing Concepts with Logical Operators

    • Disjunction (||):

      template <class T>
      concept Integral = std::is_integral_v<T>;
      
      template <class T>
      concept Real = std::is_floating_point_v<T>;
      
      template <class T>
      concept Numerical = Integral<T> || Real<T>;
    • Conjunction (&&):

      struct IComparableToInt {
        virtual bool CompareTo(int o) = 0;
      };
      
      struct IConvertibleToInt {
        virtual int ConvertTo() = 0;
      };
      
      template <class T>
      concept IntComparable = std::is_base_of_v<IComparableToInt, T>;
      
      template <class T>
      concept IntConvertible = std::is_base_of_v<IConvertibleToInt, T>;
      
      template <class T>
      concept IntComparableAndConvertible = IntComparable<T> && IntConvertible<T>;

  • Using Conjunction in Templates

    template <IntComparableAndConvertible T>
    void print(T o) {
      std::cout << o.value << '\n';
    }
    
    int main() {
      SmartNumericalValue<double> snv{42.0};
      print(snv); // OK
    
      DullNumericalValue<short> dnv{42};
      print(dnv); // Error (not IntComparable)
    }

  • Short-Circuiting Behavior
    • Conjunction (&&) evaluates left-to-right; right is only checked if left passes
    • Disjunction (||) evaluates left-to-right; right is only checked if left fails

  • Standard Library Concepts

    • Located in:
      • <concepts> (core language concepts in std)
      • <iterator> (algorithm and iterator-related concepts in std)
      • <ranges> (range-related concepts in std::ranges)
  • Examples from <concepts>:

    • same_as, integral, floating_point
    • copy_constructible, move_constructible
    • equality_comparable, totally_ordered
    • invocable, predicate
  • Examples from <iterator>:

    • sortable, permutable, mergeable
    • indirect_unary_predicate, indirect_binary_predicate
  • Examples from <ranges>:

    • range, view, input_range, forward_range, random_access_range

  • Limitations of Concepts
    • Checked only for syntax (not semantic correctness)
    • Compiler may not issue diagnostics for semantic violations
    • Concepts cannot be recursive or self-constraining

Exploring abbreviated function templates

  • Overview: Abbreviated Function Templates (C++20)
    • Simplify template function declarations
    • Use auto for parameter types instead of full template<typename> syntax
    • Enable writing generic, constrained, and variadic templates more concisely

  • 1. Unconstrained Abbreviated Function Templates
    • Syntax:
      auto sum(auto a, auto b) {
        return a + b;
      }
    • Example usage:
      auto a = sum(40, 2);    // 42
      auto b = sum(42.0, 2);  // 44.0

  • 2. Constrained Abbreviated Function Templates with Concepts
    • Syntax:
      auto sum(std::integral auto a, std::integral auto b) {
        return a + b;
      }
    • Example usage:
      auto a = sum(40, 2);     // OK
      auto b = sum(42.0, 2);   // Error (42.0 is not integral)

  • 3. Constrained Variadic Abbreviated Function Templates
    • Syntax using parameter pack and fold expression:
      auto sum(std::integral auto... args) {
        return (args + ...);
      }
    • Example usage:
      auto a = sum(10, 30, 2); // 42

  • 4. Constrained Abbreviated Lambda Expressions
    • Syntax follows same pattern:
      auto lsum = [](std::integral auto a, std::integral auto b) {
        return a + b;
      };
    • Example usage:
      auto a = lsum(40, 2);     // 42
      auto b = lsum(42.0, 2);   // Error

  • 5. Specialization of Abbreviated Function Templates

    • Standard syntax for specialization is still applicable:

      auto sum(auto a, auto b) {
        return a + b;
      }
      
      template <>
      auto sum(char const* a, char const* b) {
        return std::string(a) + std::string(b);
      }
    • Example usage:

      auto a = sum(40, 2);         // 42
      auto b = sum("40", "2");     // "402"

  • How It Works: Syntax Simplification

    • Equivalent code with traditional template syntax:
      template <typename T, typename U>
      auto sum(T a, U b) {
        return a + b;
      }
  • Constraining with Concepts (Typed Parameters)

    • Ensures only valid types are used
    • Example:
      auto sum(std::integral auto a, std::integral auto b);
    • Equivalent to:
      template <typename T>
      requires std::integral<T>
      T sum(T a, T b);
  • Variadic and Lambda Compatibility

    • Works with parameter packs and lambdas using same syntax
    • No special rules needed beyond standard rules from C++20

  • Key Points
    • auto in parameter list implies templated function
    • Constraints with concepts increase type safety and clarity
    • Specializations and overloads are supported
    • Lambdas can also use constrained abbreviated syntax
    • Greatly reduces boilerplate for simple and moderately generic functions

Iterating over collections with the ranges library

  • Purpose of the Ranges Library (C++20)
    • Simplifies working with collections
    • Replaces explicit iterator-based code with readable range-based syntax
    • Provides lazy-evaluated views and constrained algorithms
    • Available in <ranges> under std::ranges

  • Helper Aliases Used in Examples
    namespace rv = std::ranges::views;
    namespace rg = std::ranges;

  • Example Helper Function Used in Filters
    bool is_prime(int number) {
      if (number != 2) {
        if (number < 2 || number % 2 == 0) return false;
        auto root = std::sqrt(number);
        for (int i = 3; i <= root; i += 2)
          if (number % i == 0) return false;
      }
      return true;
    }

  • Common View Adaptors (Lazy, Composable, Non-owning)

    • views::iota(start, end) – Generates consecutive integers

      for (auto i : rv::iota(1, 10)) std::cout << i << ' '; // 1 2 ... 9
    • views::filter(predicate) – Filters elements using a predicate

      for (auto i : rv::iota(1, 100) | rv::filter(is_prime)) std::cout << i << ' ';
      std::vector<int> nums{1, 1, 2, 3, 5, 8, 13, 21};
      for (auto i : nums | rv::filter(is_prime)) std::cout << i << ' ';
    • views::transform(func) – Applies a function to each element

      for (auto i : rv::iota(1, 100) |
                   rv::filter(is_prime) |
                   rv::transform([](int n) { return n + 1; }))
        std::cout << i << ' ';
    • views::take(n) – Retains only the first N elements

      for (auto i : rv::iota(1, 100) |
                   rv::filter(is_prime) |
                   rv::take(10))
        std::cout << i << ' ';
    • views::reverse – Iterates a view in reverse order

      // First 10 primes in reverse order
      for (auto i : rv::iota(1, 100) |
                   rv::reverse |
                   rv::filter(is_prime) |
                   rv::take(10))
        std::cout << i << ' ';
      
      // Last 10 primes in forward order
      for (auto i : rv::iota(1, 100) |
                   rv::reverse |
                   rv::filter(is_prime) |
                   rv::take(10) |
                   rv::reverse)
        std::cout << i << ' ';
    • views::drop(n) – Skips the first N elements

      for (auto i : rv::iota(1, 100) |
                   rv::filter(is_prime) |
                   rv::drop(10) |
                   rv::reverse |
                   rv::drop(10) |
                   rv::reverse)
        std::cout << i << ' '; // middle primes

  • Algorithms with Ranges (std::ranges)

    • ranges::max(range)

      std::vector<int> v{5, 2, 7, 1, 4, 2, 9, 5};
      auto m = rg::max(v); // 9
    • ranges::sort(range)

      rg::sort(v); // Sorted
    • ranges::copy(range, output_iterator)

      rg::copy(v, std::ostream_iterator<int>(std::cout, " "));
    • ranges::reverse(range)

      rg::reverse(v); // Reverses in place
    • ranges::count_if(range, predicate)

      auto primes = rg::count_if(v, is_prime);

  • How Views Work (Lazy Evaluation)

    std::vector<int> nums{1, 1, 2, 3, 5, 8, 13, 21};
    auto v = nums | rv::filter(is_prime) | rv::take(3) | rv::reverse;
    for (auto i : v) std::cout << i << ' '; // prints 5 3 2
    • Equivalent form without pipe syntax:

      auto v = rv::reverse(
                 rv::take(
                   rv::filter(nums, is_prime), 3));
    • Pipe (|) is overloaded to simplify view composition

    • Equivalencies:

      • A(R)R | A
      • A(R, args...)A(args...)(R)R | A(args...)

  • Types of Range Concepts and Corresponding Iterator Categories

    Concept Iterator Type Capability
    input_range input_iterator Single-pass read iteration
    output_range output_iterator Write iteration
    forward_range forward_iterator Multi-pass iteration
    bidirectional_range bidirectional_iterator Forward and reverse iteration
    random_access_range random_access_iterator Constant-time indexed access
    contiguous_range contiguous_iterator Elements stored contiguously in memory

  • Standard Containers and Range Concepts
Container Input range Forward range Bidirectional range Random access range Contiguous range
forward_list
list
dequeue
array
vector
set
map
multiset
multimap
unordered_set
unordered_map
unordered_multiset
unordered_multimap

  • Additional Components in <ranges>
    • Range Concepts: range, view, sized_range, etc.
    • Range Factories: empty_view, single_view, iota_view
    • Views: filter_view, transform_view, take_view, drop_view, reverse_view, etc.
    • All views are non-owning and lazily evaluated

  • Compatibility with Range-v3 (Pre-C++20)
    • Works with C++17 using the range-v3 library
    • Required headers:
      #include "range/v3/view.hpp"
      #include "range/v3/algorithm/sort.hpp"
      #include "range/v3/algorithm/copy.hpp"
      #include "range/v3/algorithm/reverse.hpp"
      #include "range/v3/algorithm/count_if.hpp"
      #include "range/v3/algorithm/max.hpp"
      namespace rv = ranges::views;
      namespace rg = ranges;
    • All previous examples work unchanged with this setup

Exploring the standard range adaptors

  • Namespace and Headers
    • Aliases used:
      namespace rv = std::ranges::views;
      namespace rg = std::ranges;
    • Required headers:
      #include <ranges>
      #include <algorithm>

  • C++20 Range Adaptors

    • views::filter – Filters elements using a predicate

      auto primes = numbers | rv::filter(is_prime);
    • views::transform – Transforms each element with a function

      auto letters = numbers | rv::transform([](int i) { return 'A' + i; });
    • views::take – Takes the first N elements

      auto first_three = numbers | rv::take(3);
    • views::take_while – Takes elements while predicate is true

      auto less_than_three = numbers | rv::take_while([](int i) { return i < 3; });
    • views::drop – Skips the first N elements

      auto skipped = numbers | rv::drop(3);
    • views::drop_while – Skips while predicate is true

      auto after_three = numbers | rv::drop_while([](int i) { return i < 3; });
    • views::join – Flattens a range of ranges

      auto flat = nested_vectors | rv::join;
    • views::split – Splits based on a delimiter

      auto words = text | rv::split(' ');
    • views::lazy_split – Lazily splits input ranges

      auto words = text | rv::lazy_split(' ');
    • views::reverse – Reverses elements

      auto reversed = numbers | rv::reverse;
    • views::elements<N> – Extracts N-th element from tuples

      auto keys = tuples | rv::elements<0>;
      auto values = tuples | rv::elements<1>;
    • views::keys / views::values – Aliases for elements<0> / elements<1>

      auto keys = pairs | rv::keys;
      auto values = pairs | rv::values;

  • C++23 Range Adaptors

    • views::enumerate – Adds index to elements

      auto indexed = items | rv::enumerate;
    • views::zip – Combines elements from multiple ranges

      auto zipped = rv::zip(numbers, words);
    • views::zip_transform – Applies function to zipped elements

      auto combined = rv::zip_transform(f, range1, range2);
    • views::adjacent<N> – Sliding window of N as tuples

      auto triples = numbers | rv::adjacent<3>;
    • views::adjacent_transform<N> – Applies function to N-adjacent elements

      auto products = numbers | rv::adjacent_transform<3>([](a, b, c) { return a * b * c; });
    • views::join_with(delimiter) – Flatten range of ranges, inserting delimiter

      auto joined = nested_vectors | rv::join_with(0);
    • views::slide(N) – Runtime-size sliding windows (views of views)

      auto slides = numbers | rv::slide(3);
    • views::chunk(N) – Divides into N-sized chunks

      auto chunks = numbers | rv::chunk(3);
    • views::chunk_by(predicate) – Chunks based on adjacent predicate

      auto grouped = numbers | rv::chunk_by([](a, b) { return a * b % 2 == 1; });
    • views::stride(N) – Skips elements in steps of N

      auto strided = numbers | rv::stride(3);
    • views::cartesian_product – Cartesian product of multiple views

      auto product = rv::cartesian_product(range1, range2);

  • Key Differences and Usage Notes

    • adjacent<N> vs. slide(N)

      • adjacent<N>: window size known at compile-time, produces tuples
      • slide(N): window size known at runtime, produces subviews

      Original Range R:

      [1, 1, 2, 3, 5, 8, 13]
      

      adjacent_view<3>(R):

      Creates tuples of 3 adjacent elements:

      [1, 1, 2]
        [1, 2, 3]
            [2, 3, 5]
              [3, 5, 8]
                  [5, 8, 13]
      

      slide_view(R, 3): Creates a sliding window of size 3 over the range R, similar in result to adjacent_view, but aligns windows differently:

      1st element:   [1, 1, 2]
      2nd element:   [1, 2, 3]
      3rd element:   [2, 3, 5]
      4th element:   [3, 5, 8]
      5th element:   [5, 8, 13]
      
  • split vs. lazy_split

    • split: eager evaluation, works with forward_range or better
    • lazy_split: lazy evaluation, supports input_range and const ranges
  • join vs. join_with

    • join: flattens ranges
    • join_with: flattens and inserts delimiter between ranges

  • Practical Use of Compositions

    auto view = numbers | rv::filter(is_prime)
                         | rv::take(3)
                         | rv::reverse;
    
    // Equivalent:
    auto view = rv::reverse(
                  rv::take(
                    rv::filter(numbers, is_prime), 3));
  • Operator Equivalences

    • R | AA(R)
    • R | A(args...)A(args...)(R)

Converting a range to a container

  • Purpose of std::ranges::to (C++23)
    • Converts a range (often resulting from adaptors) into a container
    • Enables easy, declarative conversion of views into concrete data structures
    • Available in <ranges> header
    • Requires parentheses in pipe form: | std::ranges::to<Container>()

  • Convert Range to std::vector

    std::vector<int> numbers{ 1, 1, 2, 3, 5, 8, 13 };
    std::vector<int> primes = numbers |
        std::views::filter(is_prime) |
        std::ranges::to<std::vector>();
  • Convert Lazy Split to Vector of Strings

    std::string text{ "server=demo123;db=optimus" };
    auto parts = text |
        std::views::lazy_split(';') |
        std::ranges::to<std::vector<std::string>>();
  • Convert Zipped Range to std::unordered_multimap

    std::vector<int> numbers{ 1, 1, 2, 3, 5, 8, 13 };
    std::vector<std::string> words{ "one", "two", "three", "four" };
    auto zipped = std::views::zip(numbers, words) |
        std::ranges::to<std::unordered_multimap<int, std::string>>();
  • Convert Range to std::string

    std::string text{ "server=demo123;db=optimus" };
    std::string result = text |
        std::views::stride(3) |
        std::ranges::to<std::string>();
  • Convert Between Containers

    • Convert std::vector<int> to std::list<int>:

      std::vector<int> v{ 1, 2, 3 };
      std::list<int> l = v | std::ranges::to<std::list>();
    • Convert std::map to std::vector<std::pair<>>:

      std::map<int, std::string> m{ {1, "one"}, {2, "two"} };
      std::vector<std::pair<int, std::string>> v =
          m | std::ranges::to<std::vector<std::pair<int, std::string>>>();

  • Important Notes
    • Feature macro: __cpp_lib_ranges_to_container
    • Always include parentheses in pipe syntax:
      auto r = v | std::ranges::to<std::vector>(); // ✔️
      auto r = v | std::ranges::to<std::vector>;   // ❌ compile error
    • Type deduction works in simple cases (e.g., to<std::list>())
    • Explicit full type may be required for complex types (e.g., to<std::vector<std::pair<...>>>())

Creating your own range view

  • Purpose of trim_view
    • Custom view that trims (removes) elements from the beginning and end of a range
    • Elements are removed if they satisfy a unary predicate
    • Designed to integrate seamlessly with standard C++20 range adaptors

  • Namespace Aliases Used
    namespace rg = std::ranges;
    namespace rv = std::ranges::views;

  • 1. Defining the View Class

    template<rg::input_range R, typename P>
      requires rg::view<R>
    class trim_view : public rg::view_interface<trim_view<R, P>>
    {
    private:
      R base_{};
      P pred_;
      mutable rg::iterator_t<R> begin_{std::begin(base_)};
      mutable rg::iterator_t<R> end_{std::end(base_)};
      mutable bool evaluated_ = false;
    
      void ensure_evaluated() const {
        if (!evaluated_) {
          while (begin_ != std::end(base_) && pred_(*begin_))
            ++begin_;
          while (end_ != begin_ && pred_(*std::prev(end_)))
            --end_;
          evaluated_ = true;
        }
      }
    
    public:
      trim_view() = default;
    
      constexpr trim_view(R base, P pred)
        : base_(std::move(base)),
          pred_(std::move(pred)),
          begin_(std::begin(base_)),
          end_(std::end(base_)) {}
    
      constexpr R base() const & { return base_; }
      constexpr R base() && { return std::move(base_); }
      constexpr P const& pred() const { return pred_; }
    
      constexpr auto begin() const {
        ensure_evaluated(); return begin_;
      }
      constexpr auto end() const {
        ensure_evaluated(); return end_;
      }
    
      constexpr auto size() requires rg::sized_range<R> {
        return std::distance(begin_, end_);
      }
      constexpr auto size() const requires rg::sized_range<const R> {
        return std::distance(begin_, end_);
      }
    };

  • 2. Class Template Deduction Guide
    template<class R, typename P>
    trim_view(R&& base, P pred)
      -> trim_view<rg::views::all_t<R>, P>;

  • 3. Function Object + Pipe Support (for Composability)

    namespace details {
      template <typename P>
      struct trim_view_range_adaptor_closure {
        P pred_;
        constexpr trim_view_range_adaptor_closure(P pred) : pred_(pred) {}
    
        template <rg::viewable_range R>
        constexpr auto operator()(R&& r) const {
          return trim_view(std::forward<R>(r), pred_);
        }
      };
    
      struct trim_view_range_adaptor {
        template <rg::viewable_range R, typename P>
        constexpr auto operator()(R&& r, P pred) {
          return trim_view(std::forward<R>(r), pred);
        }
    
        template <typename P>
        constexpr auto operator()(P pred) {
          return trim_view_range_adaptor_closure(pred);
        }
      };
    
      template <rg::viewable_range R, typename P>
      constexpr auto operator|(R&& r, trim_view_range_adaptor_closure<P> const& a) {
        return a(std::forward<R>(r));
      }
    }

  • 4. Public View Entry Point
    namespace views {
      inline static details::trim_view_range_adaptor trim;
    }

  • How It Works
    • The trim_view class uses CRTP via view_interface
    • Stores:
      • base range
      • predicate
      • lazy-evaluated begin_ and end_
    • On first use (e.g., iteration), the ensure_evaluated() function:
      • Advances begin_ past elements satisfying the predicate
      • Retreats end_ before elements satisfying the predicate

  • Example

    auto is_odd = [](int n) { return n % 2 == 1; };
    std::vector<int> n{1, 1, 2, 3, 5, 6, 4, 7, 7, 9};
    
    auto v = trim_view(n, is_odd);
    rg::copy(v, std::ostream_iterator<int>(std::cout, " ")); // 2 3 5 6 4
    
    for (auto i : rv::reverse(trim_view(n, is_odd)))
      std::cout << i << ' '; // 4 6 5 3 2
  • With Pipe Syntax (Thanks to Function Objects)

    auto v = n | views::trim(is_odd);
    rg::copy(v, std::ostream_iterator<int>(std::cout, " "));
    
    for (auto i : n | views::trim(is_odd) | rv::reverse)
      std::cout << i << ' ';

  • Notes
    • trim_view is lazy and composable
    • Can be combined with other standard views
    • The pipe overload allows chaining with standard view adaptors
    • Unlike range-v3 (which includes a trim view), C++20 standard ranges do not yet provide one
    • Could be proposed for standardization in future versions of C++

Using constrained algorithms

  • Purpose of C++20 Constrained Algorithms
    • Modern replacements for traditional STL algorithms
    • Use ranges directly (e.g., std::vector, views), not iterators
    • Provide better error messages through concepts and constraints
    • Found in <algorithm>, except std::ranges::iota() in <numeric>
    • Most have overloads that also accept iterator pairs
    • Do not support execution policies (unlike traditional algorithms)

  • Initializing Ranges

    • std::ranges::fill(range, value)

      std::vector<int> v(5);
      std::ranges::fill(v, 42); // {42, 42, 42, 42, 42}
    • std::ranges::fill_n(output_iterator, count, value)

      std::vector<int> v(10);
      std::ranges::fill_n(v.begin(), 5, 42); // {42, 42, 42, 42, 42, 0, 0, 0, 0, 0}
    • std::ranges::generate_n(output_iterator, count, generator)

      std::vector<int> v(5);
      auto i = 1;
      std::ranges::generate_n(v.begin(), v.size(), [&i] { return i * i++; });
      // {1, 4, 9, 16, 25}
    • std::ranges::iota(range, start_value)

      std::vector<int> v(5);
      std::ranges::iota(v, 1); // {1, 2, 3, 4, 5}

  • Finding Elements in a Range

    • std::ranges::find(range, value)

      auto it = std::ranges::find(v, 3);
      if (it != v.cend()) std::cout << *it;
    • std::ranges::find_if(range, predicate)

      auto it = std::ranges::find_if(v, [](int n) { return n > 10; });
    • std::ranges::find_first_of(range, values)

      std::vector<int> v{1, 1, 2, 3, 5, 8, 13};
      std::vector<int> p{5, 7, 11};
      auto it = std::ranges::find_first_of(v, p);

  • Sorting and Checking Sort Order

    • std::ranges::sort(range)

      std::ranges::sort(v); // ascending
      std::ranges::sort(v, std::ranges::greater()); // descending
    • std::ranges::is_sorted(range)

      bool sorted = std::ranges::is_sorted(v);
    • std::ranges::is_sorted_until(range)

      auto it = std::ranges::is_sorted_until(v);
      auto len = std::ranges::distance(v.cbegin(), it); // length of sorted prefix

  • Working with Views
    • Example: filtering, transforming, and taking values from a vector
      auto range = v
        | std::views::filter([](int n) { return n % 2 == 1; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(4);
      std::ranges::for_each(range, [](int n) { std::cout << n << ' '; });
      auto it = std::ranges::find_if(range, [](int n) { return n > 10; });

  • Using Projections in Constrained Algorithms

    • Projections: member pointer or callable applied before predicate or comparison

    • Example: find product by price using projection

      struct Product {
        int id;
        std::string name;
        double price;
      };
      
      std::vector<Product> products{
        {1, "pen", 15.50},
        {2, "pencil", 9.99},
        {3, "rubber", 5.0},
        {4, "ruler", 5.50},
        {5, "notebook", 12.50}
      };
      
      auto it = std::ranges::find(products, 12.50, &Product::price);
      if (it != products.end()) std::cout << it->name << '\n';
    • Example: sort by name using projection

      std::ranges::sort(products, std::ranges::less(), &Product::name);
      std::ranges::for_each(products, [](const Product& p) {
        std::cout << std::format("{} = {}\n", p.name, p.price);
      });

  • Advantages of Constrained Algorithms
    • Eliminate need for .begin() and .end() calls
    • Support ranges and views directly
    • Use concepts for better compile-time errors
    • Support projections for filtering/comparing on member values
    • Provide overloads for both containers and iterators (with few exceptions)

  • Limitations
    • No support for execution policies (std::execution::par, etc.)
    • Not all traditional algorithms are mirrored with constrained versions

  • Technical Example: Signature of std::ranges::find()
    template <ranges::input_range R, class T, class Proj = std::identity>
    requires std::indirect_binary_predicate<
      ranges::equal_to,
      std::projected<ranges::iterator_t<R>, Proj>,
      const T*>
    constexpr ranges::borrowed_iterator_t<R>
    find(R&& r, const T& value, Proj proj = {});

  • Iterator-based Overload Example

    auto it = std::ranges::find(v.begin(), v.end(), 3);
  • Summary

    • Prefer constrained algorithms when possible for clarity and composability
    • Use projections to simplify comparisons and predicates
    • Combine with views for powerful and expressive pipeline-style logic

Creating a coroutine task type for asynchronous computations

-- Maybe later