Lecture 7. STL Functions and Utilities

Date: 2023-06-06

1. Introduction

Within the expansive suite of tools offered by the C++ Standard Template Library (STL), certain utilities often go underutilized by developers. Among these, std::function, functors, and other STL utilities play key roles in enhancing code flexibility and maintainability.


2. std::function (since C++11)

Overview:

Introduced in C++11, std::function is a template class that offers a type-safe and flexible wrapper for callable entities, including functions, lambda expressions, and functors. The core advantage of std::function is its capacity to reference diverse callable objects, thus enhancing code adaptability. Just as you might use int or double to denote types of variables, you can use std::function to denote types of callable objects, be it functions, lambdas, or functors. To use std::function, you must do:

#include <functional>

Syntax:

The standard declaration for std::function is:

std::function<return_type(arg_types)>

To represent a callable that takes an int and yields a double, one would use:

std::function<double(int)>

Use Cases & Code Examples:

1. Storing callbacks in containers:

A common use of std::function is to keep different callable entities in a container, which wouldn't be feasible with just function pointers.

std::vector<std::function<void()>> callbacks;
callbacks.push_back([](){ std::cout << "Lambda called!" << std::endl; });
callbacks.push_back([](){ std::cout << "Another lambda called!" << std::endl; });
for(auto& callback : callbacks) {
    callback();
}

2. Designing event-driven architectures:

In event-driven programming, the response to different events can be managed using std::function.

std::map<std::string, std::function<void()>> eventHandlers;
eventHandlers["onClick"] = [](){ std::cout << "Button clicked!" << std::endl; };
eventHandlers["onHover"] = [](){ std::cout << "Button hovered!" << std::endl; };

3. Implementing strategy patterns:

std::function allows easy interchangeability of algorithms.

std::function<int(int, int)> strategy = [](int a, int b){ return a + b; };
std::cout << "Result: " << strategy(3, 4) << std::endl;  // Outputs: Result: 7
strategy = [](int a, int b){ return a * b; };
std::cout << "Result: " << strategy(3, 4) << std::endl;  // Outputs: Result: 12

3. Functors (Function Objects)

Definition:

Functors, also known as function objects, are objects of a class that can be called as if they were ordinary functions. This is achieved by defining the operator() for a class, thus allowing objects of that class to be invoked using the typical function call syntax.

Advantages:

  1. State Retention: Unlike regular functions, functors can retain state between calls since they're objects. This can be useful for operations where state needs to be preserved.
  2. Flexibility: Functors can have member variables, methods, and even inherit from other classes, offering more flexibility than ordinary functions or lambdas.

Custom Functors:

Syntax and Structure:

A functor is typically a struct or class that has the operator() overloaded. This operator allows the object to be called.

struct MyFunctor {
    void operator()(int x) {
        // Body of the functor
    }
};

Example of a Custom Functor:

Here's an example of a functor that retains a running sum of numbers passed to it:

struct Sum {
    int runningTotal = 0;
    void operator()(int x) {
        runningTotal += x;
        std::cout << runningTotal << std::endl;
    }
};

// Usage:
Sum s;
s(5);  // Prints 5
s(3);  // Prints 8

Predefined Functors:

C++ STL provides a set of predefined functors that can be very handy for common operations.

Arithmetic:

  1. plus: Adds two elements.
std::plus<int>()(3, 5);  // Returns 8
  1. minus: Subtracts the second element from the first.
std::minus<int>()(5, 3);  // Returns 2
  1. multiplies: Multiplies two elements.
std::multiplies<int>()(3, 5);  // Returns 15

... and so on for divides, modulus, etc.

Comparison:

  1. less: Checks if the first element is less than the second.
std::less<int>()(3, 5);  // Returns true
  1. greater: Checks if the first element is greater than the second.
std::greater<int>()(5, 3);  // Returns true
  1. not_equal_to: Checks if two elements are not equal.
std::not_equal_to<int>()(5, 5);  // Returns false

... and similar for equal_to, greater_equal, etc.

Logical:

  1. logical_and: Returns logical AND of two values.
std::logical_and<bool>()(true, false);  // Returns false
  1. logical_or: Returns logical OR of two values.
std::logical_or<bool>()(true, false);  // Returns true
  1. logical_not: Returns logical NOT of a value.
std::logical_not<bool>()(true);  // Returns false

Utilities:

There are various utility functors like negate, which will return the negation of a given value:

std::negate<int>()(5);  // Returns -5

4. Utilities

Pairs:

  • pair: A pair can hold two values, possibly of different types. Handy when you need to store two related values together.
std::pair<int, std::string> p(1, "One");
  • Common member functions:

  • first: Accesses the first value.

  • second: Accesses the second value.
std::cout << p.first;  // 1
std::cout << p.second; // "One"
  • make_pair: Create a pair without typing out the types.
auto p = std::make_pair(2, "Two");

Tuples (since C++11):

  • Definition: Tuples can hold any number of values of different types, extending the concept of pairs.

  • Member functions and utilities:

  • get: Get a value from a tuple by its index.

std::tuple<int, double, std::string> t(1, 3.14, "Hello");
std::cout << std::get<1>(t); // 3.14
  • tuple_size: Returns the number of elements in the tuple.
  • tuple_cat: Merges two or more tuples.
  • tie: Unpacks a tuple into variables.
int a;
double b;
std::string s;
std::tie(a, b, s) = t;
  • make_tuple: Instantly creates a tuple.
auto t = std::make_tuple(2, 4.5, "World");

Other Utilities:

  • std::apply: Used to call a function with arguments provided in a tuple.
auto print = [](int x, int y) { std::cout << x << ", " << y; };
std::tuple<int, int> args = std::make_tuple(3, 4);
std::apply(print, args);  // Outputs: 3, 4
  • std::invoke: Invokes a callable object with specified arguments.
int add(int a, int b) { return a + b; }
std::cout << std::invoke(add, 3, 4); // 7
  • std::reference_wrapper: Useful for storing references in containers.
int x = 10, y = 20;
std::vector<std::reference_wrapper<int>> vec = {x, y};
vec[0].get() = 15; // x is now 15
  • std::optional: Represents an optional value.
std::optional<int> divide(int a, int b) {
    if(b == 0) return std::nullopt; // No value
    return a/b;
}
auto result = divide(5, 0); 
if(result) std::cout << *result; // Not printed
  • std::bind: Generates a callable object by binding one or more arguments to a function.
int add(int a, int b) { return a + b; }
auto boundFunc = std::bind(add, std::placeholders::_1, 5);
std::cout << boundFunc(3); // Outputs 8 because 3 + 5 = 8
  • std::any (since C++17): A type-safe container for single values of any type.
std::any a = 42;
a = std::string("Hello");
if (a.has_value()) std::cout << std::any_cast<std::string>(a); // Outputs "Hello"
  • std::variant (since C++17): A type-safe union.
std::variant<int, double, std::string> v;
v = 12;
v = 3.14;
v = "Hello";
  • std::holds_alternative (since C++17): Check the type held by a variant.
if (std::holds_alternative<std::string>(v)) {
    std::cout << "Holds a string!";
}
  • std::filesystem (since C++17): Provides a standard way to manipulate files, directories, and paths in a filesystem.
std::filesystem::path p = "/tmp/example.txt";
if (std::filesystem::exists(p)) {
    std::cout << p << " exists!";
}
  • std::chrono: Allows you to handle time in a type-safe manner.
std::chrono::milliseconds ms(1000);
std::chrono::seconds s = std::chrono::duration_cast<std::chrono::seconds>(ms);
std::cout << s.count();  // 1

5. Lambda Expressions

Definition:

Lambda expressions, often simply called "lambdas", are a feature introduced in C++11 that allow you to define anonymous (unnamed) functions directly in the place where they're used, typically as arguments to functions. They can capture variables from their enclosing scope, providing a quick and powerful way to create function objects.

Basic Syntax:

[ capture_clause ] ( parameters ) -> return_type { body_of_lambda }
  • capture_clause: Specifies which outside variables are available for the lambda.
  • parameters: Just like function parameters.
  • return_type: Optional. If skipped, it's inferred from the return statements in the lambda.
  • body_of_lambda: The code the lambda should execute.

Examples:

  1. Basic Lambda:
auto greet = []() {
    std::cout << "Hello, World!" << std::endl;
};
greet();  // Outputs: Hello, World!
  1. Lambda with Parameters:
auto add = [](int a, int b) {
    return a + b;
};
std::cout << add(2, 3);  // Outputs: 5
  1. Lambda with Capture:
int x = 10;
auto addX = [x](int a) {
    return a + x;
};
std::cout << addX(5);  // Outputs: 15

Capture Modes:

  • [=]: Capture all local variables by value.
  • [&]: Capture all local variables by reference.
  • [x, &y]: Capture x by value and y by reference.

Use Cases:

  1. Short Callbacks: Lambdas are often used for short callback functions, especially with STL algorithms.
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), [](int num) {
    std::cout << num * num << " ";  // Print the square of the number
});
  1. Custom Sort:
std::sort(vec.begin(), vec.end(), [](int a, int b) {
    return a > b;  // Sort in descending order
});
  1. Temporary Function Objects: When you need a one-off function object and don't want to define a whole functor class.

Advantages:

  1. Readability: Can make the code more concise and readable by keeping the function logic close to where it's used.
  2. Flexibility: Provides a quick way to define custom logic on-the-fly.
  3. Encapsulation: Can capture local variables and use them, making them ideal for tasks like custom comparisons or transformations based on local data.

6. Q&A - std::function

Q1: How can I use std::function to store a simple function?

A1: You can define a std::function variable and assign a simple function to it. If the function takes an int and returns a double, for instance, the std::function type would be std::function<double(int)>.

double half(int x) {
    return x / 2.0;
}

int main() {
    std::function<double(int)> func = half;
    std::cout << func(8);  // Outputs: 4.0
}

Q2: Can I use std::function to store a lambda expression?

A2: Absolutely! std::function can store any callable object, including lambdas.

int main() {
    std::function<int(int, int)> add = [](int a, int b) {
        return a + b;
    };
    std::cout << add(3, 4);  // Outputs: 7
}

Q3: How about storing a member function of a class in std::function?

A3: Member functions have an implicit this pointer, so you need to use std::bind or a lambda to wrap the member function with an instance of the object.

class Multiplier {
public:
    int factor;
    Multiplier(int f) : factor(f) {}
    int multiply(int x) {
        return x * factor;
    }
};

int main() {
    Multiplier m(3);
    std::function<int(int)> func = std::bind(&Multiplier::multiply, &m, std::placeholders::_1);
    // Or with lambda: std::function<int(int)> func = [&m](int x) { return m.multiply(x); };
    std::cout << func(4);  // Outputs: 12
}

Q4: Can std::function store a functor (function object)?

A4: Yes, it can!

struct Doubler {
    int operator()(int x) {
        return x * 2;
    }
};

int main() {
    std::function<int(int)> func = Doubler();
    std::cout << func(5);  // Outputs: 10
}

Q5: What happens if I try to call a std::function that has not been assigned a callable object?

A5: It will throw a std::bad_function_call exception. You should always ensure that your std::function is assigned a callable before invoking it, or handle the potential exception.

int main() {
    std::function<void()> func;
    try {
        func();  // This will throw an exception
    } catch (const std::bad_function_call& e) {
        std::cout << "Error: " << e.what();
    }
}

7. Q&A - Functors

Q1: What exactly is a functor, and how does it differ from a regular function?

A1: A functor, or function object, is an object of a class that has the operator() defined. Unlike a regular function, a functor can have a state that it remembers between calls.

struct Counter {
    int value;

    Counter(int start) : value(start) {}

    int operator()() {
        return value++;
    }
};

int main() {
    Counter counter(5);
    std::cout << counter() << " ";  // Outputs: 5
    std::cout << counter();         // Outputs: 6
}

Q2: How can I use a functor as a custom comparator in STL algorithms or containers?

A2: Functors are commonly used as custom comparators, especially in STL functions like sort or containers like set.

struct CaseInsensitiveCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), 
            [](char charA, char charB) {
                return std::tolower(charA) < std::tolower(charB);
            });
    }
};

int main() {
    std::set<std::string, CaseInsensitiveCompare> mySet;
    mySet.insert("apple");
    mySet.insert("Apple");
    std::cout << mySet.size();  // Outputs: 1 since "apple" and "Apple" are considered the same
}

Q3: Can a functor have multiple overloaded operator() functions?

A3: Yes, a functor can have multiple overloaded operator() functions, effectively allowing it to be called with different argument types or numbers of arguments.

struct OverloadedFunctor {
    void operator()(int x) {
        std::cout << "Integer: " << x << std::endl;
    }

    void operator()(double x) {
        std::cout << "Double: " << x << std::endl;
    }
};

int main() {
    OverloadedFunctor f;
    f(10);     // Outputs: Integer: 10
    f(5.5);    // Outputs: Double: 5.5
}

Q4: Can I use functors to encapsulate complex logic or calculations?

A4: Absolutely! Functors are great for encapsulating complex logic or multi-step calculations. Their ability to maintain state allows them to be used in scenarios like accumulating results.

struct ComplexCalculator {
    double result = 1;

    void operator()(double value, double factor) {
        result += value * factor;
    }

    double getResult() const {
        return result;
    }
};

int main() {
    ComplexCalculator calc;
    calc(2.5, 3);
    calc(4, 0.5);
    std::cout << calc.getResult();  // Outputs: 9.5
}

Q5: Can functors be used with standard algorithms like std::transform or std::for_each?

A5: Yes! Functors are often used with standard algorithms to provide custom behavior.

struct Square {
    int operator()(int x) {
        return x * x;
    }
};

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    std::transform(nums.begin(), nums.end(), nums.begin(), Square());

    for(int val : nums) {
        std::cout << val << " ";  // Outputs: 1 4 9 16 25
    }
}

8. Q&A - Utilities

Q1: How can I quickly create and use pairs using std::pair?

A1: The std::pair is a simple structure that holds two values. It's useful when you want to return two values from a function or store two related items together.

#include <iostream>
#include <utility>

int main() {
    std::pair<int, std::string> student = std::make_pair(101, "Alice");
    std::cout << "ID: " << student.first << ", Name: " << student.second;  // Outputs: ID: 101, Name: Alice
}

Q2: What are tuples and how are they different from pairs?

A2: std::tuple is an extension of std::pair that can hold more than two values. They are flexible containers for heterogeneous data.

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, double, std::string> item(1, 5.5, "apple");
    std::cout << std::get<2>(item);  // Outputs: apple
}

Q3: How can I use smart pointers to manage the lifetime of my objects?

A3: Smart pointers like std::unique_ptr and std::shared_ptr automatically manage the lifetime of objects, ensuring no memory leaks.

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(10));
    std::cout << *ptr;  // Outputs: 10

    // No need to delete ptr; it gets destroyed automatically when going out of scope.
}

Q4: How does std::move work and when should I use it?

A4: std::move is used to indicate that an object may be "moved from", transferring ownership of resources rather than copying them. It's used to optimize performance.

#include <iostream>
#include <vector>
#include <utility>

int main() {
    std::vector<int> oldVec = {1, 2, 3};
    std::vector<int> newVec = std::move(oldVec);

    std::cout << newVec.size();  // Outputs: 3
    std::cout << oldVec.size();  // Outputs: 0 since oldVec no longer owns the data
}

Q5: What is std::forward and where might I see it being used?

A5: std::forward is used in conjunction with rvalue references to implement "perfect forwarding". It's often seen in template functions to preserve the "lvalue-ness" or "rvalue-ness" of passed arguments.

#include <iostream>
#include <utility>

template<typename Func, typename Arg>
void wrapper(Func func, Arg&& arg) {
    func(std::forward<Arg>(arg));
}

void printStr(const std::string& s) {
    std::cout << "Lvalue string: " << s << std::endl;
}

void printStr(std::string&& s) {
    std::cout << "Rvalue string: " << s << std::endl;
}

int main() {
    std::string str = "test";
    wrapper(printStr, str);             // Calls lvalue version
    wrapper(printStr, "temporary");     // Calls rvalue version
}

Q6: What does std::invoke do and how is it used?

A6: std::invoke is used to call a callable object for the given arguments. It's versatile because it can call regular functions, lambda functions, member functions, and functors.

#include <iostream>
#include <functional>

struct Adder {
    int operator()(int a, int b) { return a + b; }
};

int main() {
    Adder add;
    std::cout << std::invoke(add, 3, 4);  // Outputs: 7
}

Q7: How can std::optional be used to represent the absence or presence of a value?

A7: std::optional is a container that might or might not contain a value. It can be used to represent operations that might fail without throwing exceptions.

#include <iostream>
#include <optional>

std::optional<int> safeDivide(int a, int b) {
    if (b == 0) return {};  // return no value
    return a / b;
}

int main() {
    auto result = safeDivide(10, 2);
    if (result) std::cout << "Result: " << *result;  // Outputs: Result: 5
}

Q8: How can I bind arguments to a function using std::bind?

A8: std::bind creates a new callable object by binding one or more arguments to a function. Placeholders can be used to specify which arguments remain unbound.

#include <iostream>
#include <functional>

int multiply(int a, int b) { return a * b; }

int main() {
    auto boundMultiply = std::bind(multiply, 2, std::placeholders::_1);
    std::cout << boundMultiply(3);  // Outputs: 6 (because 2*3=6)
}

Q9: Why and how should I use std::reference_wrapper?

A9: std::reference_wrapper is a way to store references inside STL containers. Since normal references can't be reassigned, reference_wrapper comes in handy.

#include <iostream>
#include <vector>
#include <functional>

int main() {
    int a = 1, b = 2, c = 3;
    std::vector<std::reference_wrapper<int>> refs = {a, b, c};
    for (auto &ref : refs) ref.get() *= 2;
    std::cout << a << ", " << b << ", " << c;  // Outputs: 2, 4, 6
}

Q10: How can I utilize std::filesystem for path manipulations?

A10: std::filesystem provides functions and classes to work with files and directories. It's a cross-platform way to interact with the filesystem.

#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::path filePath = "path/to/file.txt";
    if (std::filesystem::exists(filePath)) {
        std::cout << "Size: " << std::filesystem::file_size(filePath) << " bytes";
    } else {
        std::cout << filePath << " does not exist.";
    }
}

Q11: How does std::tie help in unpacking tuple values?

A11: std::tie creates a tuple of lvalue references, which can be used to unpack a tuple into individual variables.

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, char, std::string> data(10, 'a', "hello");
    int num;
    char ch;
    std::string str;
    std::tie(num, ch, str) = data;
    std::cout << num << ", " << ch << ", " << str;  // Outputs: 10, a, hello
}

Q12: How can I use std::any to store any type?

A12: std::any is a type-safe container that can hold any type. It's useful when you want to store an object without knowing its type beforehand.

#include <iostream>
#include <any>

int main() {
    std::any value = 42;
    value = std::string("hello");
    if (value.type() == typeid(std::string)) {
        std::cout << std::any_cast<std::string>(value);  // Outputs: hello
    }
}

Q13: What's the purpose of std::variant and how is it used?

A13: std::variant can hold one of its specified types at a time. It's a type-safe union and useful when dealing with multiple potential types for a single variable.

#include <iostream>
#include <variant>

using IntOrString = std::variant<int, std::string>;

int main() {
    IntOrString value = 5;
    std::visit([](auto&& arg) { std::cout << arg; }, value);  // Outputs: 5
}

Q14: How can std::chrono be used for time-related operations?

A14: std::chrono provides utilities to deal with date and time. It's great for measuring durations, performing time arithmetic, and more.

#include <iostream>
#include <chrono>
#include <thread>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    std::this_thread::sleep_for(std::chrono::seconds(2));
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::seconds>(end - start).count();
    std::cout << "Slept for " << duration << " seconds";  // Outputs: Slept for 2 seconds
}

Q15: How can I use std::bitset for manipulating bit sequences?

A15: std::bitset is a class that represents a fixed-size sequence of N bits. It provides utility functions to manipulate bits in various ways.

#include <iostream>
#include <bitset>

int main() {
    std::bitset<8> bits("10100100");
    bits.set(2, 1);  // Set bit at position 2 to 1
    std::cout << bits;  // Outputs: 10101100
}

9. Q&A - Lambda Expressions

Q1: What is a lambda expression in C++ and how can it be defined?

A1: A lambda expression is a concise way to define an anonymous function object (a function without a name) directly at the location where it is invoked or passed as an argument to a function.

auto lambda = [](int x, int y) -> int {
    return x + y;
};
std::cout << lambda(2, 3);  // Outputs: 5

Q2: How can you capture variables from the enclosing scope in a lambda expression?

A2: Lambdas can capture variables from their enclosing scope by value or by reference using capture clauses:

  • []: No variables are captured.
  • [x, y]: Captures x and y by value.
  • [&x, &y]: Captures x and y by reference.
  • [=]: Captures all local variables by value.
  • [&]: Captures all local variables by reference.
int a = 1, b = 2;
auto sum = [a, &b]() { 
    b += a; 
    return b; 
};
std::cout << sum();  // Outputs: 3

Q3: Can lambda expressions be used with standard algorithms like std::sort or std::for_each?

A3: Yes, lambda expressions are especially useful with STL algorithms where you need custom behavior without defining a separate named function.

std::vector<int> nums = {3, 1, 4, 1, 5, 9};
std::sort(nums.begin(), nums.end(), [](int x, int y) {
    return x > y;  // Sort in descending order
});

Q4: What are "generic" lambdas and how are they used?

A4: Introduced in C++14, generic lambdas allow the parameter types to be auto, which lets you define a lambda function with generic parameters.

auto genericLambda = [](auto x, auto y) {
    return x + y;
};
std::cout << genericLambda(2, 3) << "\n";    // Outputs: 5
std::cout << genericLambda(2.5, 3.5) << "\n";  // Outputs: 6

Q5: Can lambda expressions have state? If so, how?

A5: Yes, lambdas can maintain state using captured variables. Additionally, starting with C++14, lambdas can have default-initialized captures, effectively providing them with local state across invocations.

auto counter = [count = 0]() mutable {
    return ++count;
};
std::cout << counter() << ", " << counter() << "\n";  // Outputs: 1, 2