The Sequence Concept and Generic Algorithms

The Sequence Concept

In C++, a concept is a named set of requirements on a type. You can think of a concept as the C++ way of specifying the interface of an abstract data type (ADT). The sequence concept enforces that a type satisfies the requirements of the sequence ADT we covered in class. To be a sequence, a type must satisfy the requirements:

  1. It is a finite-sized random-access range
  2. You can obtain the i-th element of the sequence S with the syntax S[i]
  3. You can obtain the length of the sequence S with the syntax S.size()
  4. Optionally, you can require that the value type of the elements is T

To force the value type of the sequence to be T, we can optionally write the concept as sequence<T>. The value type can also be left unspecified, so the concept sequence (without the type parameter T) matches a sequence of any type.

For example, the types par::array_sequence<int>, a std::array<int, N>, a std::vector<int>, a std::span<int>, etc., are all valid implementations of integer sequences, so they satisfy the sequence<int> concept.

Optional: If you're curious, you can see the code for the sequence concept below, but you are not required to understand it. This requires rather advanced C++ knowledge to implement.
Click to view the specification of the sequence concept
template<typename Range, typename T = void>
concept sequence = std::ranges::sized_range<Range> && std::ranges::random_access_range<Range> &&
                   (std::same_as<T, void> || std::same_as<std::range_value_t<Range>, T>) &&
                   requires(Range&& r, std::size_t i) {
                     { r[i] } -> std::same_as<std::range_reference_t<Range>>;
                     { r.size() } -> std::same_as<std::range_size_t<Range>>;
                   };

Writing Generic Algorithms Using Sequence

Functions in C++ (both ordinary and lambda functions) can be generic, meaning they can take inputs of different types. There are many ways to specify generic functions, the simplest of which is to use the auto keyword as the type of a parameter. Using auto as the type tells the compiler to deduce the type based on the type of the argument passed when calling the function.

Using auto as the type of a parameter makes the type unconstrained; the function will accept any type for which the code compiles. We can restrict the type so that it is required to match a particular concept by writing the concept name before the auto keyword.

In this class, we will frequently write generic algorithms on sequences using the pattern sequence auto, which means a generic (deduced) type that is required to match the sequence concept. For example, we can write a generic function that takes any integer sequence type (by constant reference) as

auto my_function(const sequence<int> auto& seq)

That's it! This function can now be called using an argument of any type that satisfies sequence<int>. Similarly, to write an even more generic function that can take a sequence of any value type, we can write

auto my_function(const sequence auto& seq)
Warning: Note that we should almost always take sequence arguments by const reference (const sequence<int> auto&) or forwarding references (sequence<int> auto&&) to avoid accidental copies. Since C++ uses value semantics, writing sequence<int> auto as a parameter type will typically make an expensive deep copy of the argument!