COMS W4995 C++ Deep Dive for C Programmers

Functional Programming

Function Object (aka Functor)

Consider the following program 14/functor that copies a vector v1 into v2 using a back_insert_iterator and prints out v2 twice using For_Each():

template <typename I, typename F>
void For_Each(I b, I e, F f) {
    while (b != e) {
        f(*b);
        ++b;
    }
}

void print_int(int x)  { cout << x << ' '; }

struct IntPrinter {
    void operator()(int x) const { cout << x << ' '; }
};

int main() {
  vector<int> v1 { 100, 101, 102, 103, 104 };
  vector<int> v2;
  
  // Function object (aka functor)

  copy(v1.begin(), v1.end(), back_inserter(v2));
  For_Each(v2.begin(), v2.end(), &print_int);    cout << '\n';
  For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

We’ve seen the first invocation of For_Each() using a function pointer before. The second invocation of For_Each() is new; we construct an IntPrinter object and pass it to the For_Each() function template as f. The call to f(*b) in For_Each() invokes the operator() member function of the IntPrinter object. A class that implements operator() and can therefore be invoked is called a function object or functor. Unlike a function pointer, a functor can store state information, as we’ll see later.

Stateless Functors

Shown below is an incorrect attempt to negate the integers in v2 before printing them out:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Stateless functors

    copy(v1.begin(), v1.end(), back_inserter(v2));
    For_Each(v2.begin(), v2.end(), negate<int>{});
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

std::negate is an STL functor that defines operator() as follows:

constexpr T operator()(const T& arg) const {
    return -arg;
}

Since For_Each() simply invokes f(*b) without assigning the result back to the element, the For_Each() call has no effect.

Instead, we can use the transform() algorithm to modify each element as we copy v1 to v2:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Stateless functors

    transform(v1.begin(), v1.end(), back_inserter(v2),
            negate<int>{});
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

Here’s a possible implementation for transform():

template <typename SrcIter, typename DestIter, typename F>
DestIter Transform(SrcIter b_src, SrcIter e_src, DestIter b_dest, F f) {
    while (b_src != e_src) {
        *b_dest = f(*b_src);
        ++b_dest;
        ++b_src;
    }
    return b_dest;
}

Stateful Functors

Let’s say we wanted to apply the function f(y) = 300 + y to each element of v1 as we copy it over to v2. Below, we define a functor AddX that stores a member variable x and has operator() defined to return the sum of x and a given integer y:

struct AddX {
    int x;
    AddX(int x) : x(x) {}
    int operator()(int y) const { return x + y; }
};

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Stateful functors

    transform(v1.begin(), v1.end(), back_inserter(v2),
            AddX{300});
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

The instance AddX{300} is an example of a stateful functor that carries around the number 300 to use every time operator() is invoked.

In fact, we can further generalize our functor to carry around the function to call:

template <typename F>
struct OpX {
    F op;
    int x;
    OpX(F op, int x) : op(op), x(x) {}
    int operator()(int y) const { return op(x, y); }
};

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Stateful functors

    transform(v1.begin(), v1.end(), back_inserter(v2),
            OpX{plus<int>{}, 300});
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

The OpX functor is a class template parameterized by any invocable type F. An instance of OpX holds a function object op of type F in addition to one number x to call the function object with. Its operator() will call op with x and a given integer y.

Here, we create an instance of OpX that holds the std::plus functor and the integer 300. std::plus is a binary functor, i.e., it takes two arguments. OpX binds the first argument of std::plus to 300, effectively yielding a unary functor that can be called by transform() with a single argument.

STL provides a function template, std::bind_front(), which generalizes this pattern of holding a functor and a partial list of its arguments. For example, assuming f is a functor that takes five arguments, auto g = bind_front(f, arg1, arg2, arg3) would return a functor g that takes two arguments. In other words, f(arg1, arg2, arg3, arg4, arg5) is equivalent to g(arg4, arg5).

We can rewrite our program to use bind_front() as follows:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Stateful functors

    transform(v1.begin(), v1.end(), back_inserter(v2),
            bind_front(plus<int>{}, 300));
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

Understanding how bind_front() works requires additional template machinery that we have yet to study. We’ll revisit this machinery in later chapters.

Bind expressions

std::bind_front() was introduced in C++20 as a streamlined replacement for std::bind(), which has been around since C++11. std::bind() was designed to support complex bind operations – it can bind any of the functor’s arguments in any order and even compose multiple functors. std::bind_front() doesn’t support these features, but it is more efficient and easier to use than std::bind() for the common case of binding a partial list of arguments to a functor.

std::bind() uses what’s known as “placeholders” to specify unbound arguments that will later be passed to the functor it returns. Consider the following example:

struct SubSubSub {
    int operator()(int a, int b, int c, int d) const {
        return a - b - c - d;
    }
};
auto f = bind(SubSubSub{}, 1000,
              placeholders::_2, placeholders::_1, placeholders::_1);
cout << f(100, 500) << '\n';

This snippet will compute 1000 - 500 - 100 - 100 and print 300. The SubSubSub functor takes four arguments: a, b, c, and d. We bind the first argument a to 1000 but leave the other three arguments unbounded by passing placeholder objects. The std::placeholders namespace defines global variabes _1, _2, etc., that we can use to specify how the arguments to the functor f will be passed to the SubSubSub functor. In our example, the first argument to f will be routed to c and d, and the second argument will be routed to b.

We can rewrite the bind_front() call we used in the last section using bind() as follows:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Bind expressions

    transform(v1.begin(), v1.end(), back_inserter(v2),
            bind(plus<int>{}, 300, placeholders::_1));
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

std::bind() also supports function composition. Suppose we want to implement a functor for f(x) = 300 + g(x), where g(x) = -x. std::bind() lets us compose f(x) using std::plus and std::negate as follows:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Bind expressions

    transform(v1.begin(), v1.end(), back_inserter(v2),
            bind(plus<int>{}, 300,
                bind(negate<int>{}, placeholders::_1)));
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

Here, we are passing the result of a bind() call, known as a bind expression, as an argument to another bind() call, which results in function composition. Placeholders used in a nested bind expression refer to the arguments that will later be passed to the overall bind expression.

Lambda expressions

C++11 introduced lambda expressions, which return unnamed functors. Let us replace the messy nested bind expression from the previous section with a lambda expression:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;
    
    // Lambda expressions

    transform(v1.begin(), v1.end(), back_inserter(v2),
            [] (int x) {
                return 300 + -x;
            }
    );
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

A lambda expression starts with [], which is known as the lambda introducer, and is followed by what looks like a function definition.

Closures

The lambda introducer can optionally contain captures, which specify local variables to be captured by the lambda. Here’s an example:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;

    // Lambda expressions

    int a = 2, b = 100;
    auto lambda = [&a, b] (int x) {
        return a * x + b;
    };

    a = 4, b = 1'000'000;
    transform(v1.begin(), v1.end(), back_inserter(v2), lambda);
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

The syntax [&a, b] means that the lambda captures the local variable a by reference and b by value. In the example above, the values of the local variables a and b are 2 and 100, respectively, at the time the lambda is constructed. This effectively creates a closure, a generic term in programming languages referring to a function object that captures some variables in scope.

After the lambda construction, we change a to 4 and b to 1'000'000, respectively. (Note that C++ lets you place single quotes in between digits for the sake of readability.) The change to a is reflected in the lambda because the lambda object holds a reference to a, but the change to b is not reflected because the lambda object holds a copy of b. The program produces the following output:

500 504 508 512 516 

Be careful when capturing local variables by reference. If the lambda outlives the captured variable, the lambda will hold a dangling reference, resulting in undefined behavior.

Note that we used auto for the type of the variable lambda. That is because we do not know that type of the functor that the lambda expression returns. The compiler will generate a unique type for each lambda expression on the fly.

For the lambda defined above, for example, we can imagine that the compiler will generate the following class L1 on the fly:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;

    // Translating lambda to a class

    struct L1 {
        int& a;
        int  b;
        L1(int& a, int b) : a(a), b(b) {}
        int operator()(int x) const { return a * x + b; }
    };

    int a = 2, b = 100;
    auto lambda = L1{a, b};

    a = 4, b = 1'000'000;
    transform(v1.begin(), v1.end(), back_inserter(v2), lambda);
    For_Each(v2.begin(), v2.end(), IntPrinter{});  cout << '\n';
}

Reframing lambda and closure as a concrete functor class that we’re familiar with unveils the mystery around them!

Ranges

C++20 introduces the ranges libary. A range is a generalization of a sequence of elements in STL denoted by a half-open interval, [begin, end). std::ranges::range is a concept defined as follows:

template <typename T>
concept range = requires(T& t) {
    ranges::begin(t);
    ranges::end(t);
};

T is a range if it has a begin iterator and an end sentinel, which is a generalization of the end iterator. This means that ranges::end() does not have to return an iterator. It just has to return an object that implements operator==() to test if a given iterator has reached the end of the range. All STL containers, because they have begin and end iterators, are naturally ranges.

The ranges library contains range adapters, also known as views. A view is a lightweight, read-only range that doesn’t own its elements, but wraps another range instead. A view contains a rule to apply to each element as it gets read from the underlying range, like applying a transformation.

We can rewrite our f(x) = 300 + -x transformation using the ranges library as follows:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;

    // Ranges

    auto v1_view = ranges::transform_view{
        ranges::transform_view{v1, negate<int>{}},
        bind_front(plus<int>{}, 300)
    };

    ranges::copy(v1_view, back_inserter(v2));
    ranges::for_each(v2, IntPrinter{});  cout << '\n';
}

We first construct a transform_view object with a range, our vector v1, and a rule, the functor negate. We then create another transform_view with the previous view as the range and the functor returned by bind_front() as the rule.

The ranges namespace provides range versions of the STL algorithms we’ve been working with. The new versions operate on ranges instead of begin & end iterator pairs. We use ranges::copy() to copy the elements of v1_view into v2. As v1_view is read by copy(), the transformations held in the view are applied to each element. Finally, we use ranges::for_each() to print out each element of v2.

STL overloads operator|(), which enables us to rewrite the nested view construction as a pipeline of views, as shown below:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;

    // Ranges

    auto v1_view = v1 | views::transform(negate<int>{})
                      | views::transform(bind_front(plus<int>{}, 300));

    ranges::copy(v1_view, back_inserter(v2));
    ranges::for_each(v2, IntPrinter{});  cout << '\n';
}

Here, views::transform() is a function template that takes a functor and returns what’s called a range adapter closure. The pipeline operator is overloaded to take a range R and a range adapter closure C such that R | C is equivalent to C(R). R | C returns a new range which can then be fed into the next pipeline operator. The upshot is that the expression v1 | views::transform(negate<int>{}) is equivalent to ranges::transform_view{v1, negate<int>{}}.

Instead of using ranges::copy() to back-insert each element of the view from the pipeline into our vector v2, we can append ranges::to<vector<int>>(), introduced in C++23, to the pipeline to create a new vector:

int main() {
    vector<int> v1 { 100, 101, 102, 103, 104 };
    vector<int> v2;

    // Ranges

    auto v3 = v1 | views::transform(negate<int>{})
                 | views::transform(bind_front(plus<int>{}, 300))
                 | ranges::to<vector<int>>();

    ranges::for_each(v3, IntPrinter{});  cout << '\n';
}

Here, ranges::to<vector<int>>() returns a range adapter closure, which will back-insert each element of the view from the pipeline into a newly constructed vector and return it by value. v3 gets move-constructed from the returned vector.

Finally, the pipeline below demonstrates a few more features of the ranges library:

int main() {
    auto v = views::iota(100)
             | views::take(5)
             | views::transform(negate<int>{})
             | views::transform(bind_front(plus<int>{}, 300))
             | views::filter([](int x) { return x % 2 == 0; });

    ranges::for_each(v, IntPrinter{});  cout << '\n';
}

The program’s output is as follows:

200 198 196 

We start our view pipeline with views::iota(100), which represents the infinite range of integers starting from 100. We can imagine that it is implemented using a begin iterator that starts at 100 and a sentinel object whose operator==() always evaluates to false, effectively creating an infinite range. Such ranges that generate their elements on the fly are known as range factories or generators.

We then chain views::take(5), which will take up to 5 elements from the beginning of the source range. After our two previously discussed transformations of negating and adding 300, we apply a filter view that keeps only those elements that satisfy the given predicate. We pass a lambda that checks if a given element is an even number.

The resulting view pipeline v is evaluated lazily. That is, the series of transformations only happen as ranges::for_each() attempts to read each element from the view.


Last updated: 2025-11-11