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:
- 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.
- 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:
- plus: Adds two elements.
std::plus<int>()(3, 5); // Returns 8
- minus: Subtracts the second element from the first.
std::minus<int>()(5, 3); // Returns 2
- multiplies: Multiplies two elements.
std::multiplies<int>()(3, 5); // Returns 15
... and so on for divides
, modulus
, etc.
Comparison:
- less: Checks if the first element is less than the second.
std::less<int>()(3, 5); // Returns true
- greater: Checks if the first element is greater than the second.
std::greater<int>()(5, 3); // Returns true
- 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:
- logical_and: Returns logical AND of two values.
std::logical_and<bool>()(true, false); // Returns false
- logical_or: Returns logical OR of two values.
std::logical_or<bool>()(true, false); // Returns true
- 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:
- Basic Lambda:
auto greet = []() {
std::cout << "Hello, World!" << std::endl;
};
greet(); // Outputs: Hello, World!
- Lambda with Parameters:
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(2, 3); // Outputs: 5
- 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 andy
by reference.
Use Cases:
- 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
});
- Custom Sort:
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // Sort in descending order
});
- Temporary Function Objects: When you need a one-off function object and don't want to define a whole functor class.
Advantages:
- Readability: Can make the code more concise and readable by keeping the function logic close to where it's used.
- Flexibility: Provides a quick way to define custom logic on-the-fly.
- 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]
: Capturesx
andy
by value.[&x, &y]
: Capturesx
andy
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