Modern C++
Standard Practices from Standard Library


3. STL Algorithms and Lambda Expressions


Stefano Lusardi, Software Developer

Agenda


  • std::algorithms
  • Lambda Functions
  • auto keyword

std::algorithms



What is an std::algorithm?


It is a functions designed to address general purpose problems that can be used on a range of elements.

Since the STL is just all about Containers, Iterators and Algorithms it is designed to have the best possible interaction between these components


See the full reference here

Why do we need an algorithm library
(in Modern C++) ?


Expressive, Solid, and Reusable chunks of code.


Do you remember the initial example given in the first talk?



// 1. "Raw" for loop
int c = 0;
for (int i = 0; i < myVec.size(); ++i) {
    myVec[i] == 5 && ++c;
}
						



// 2. Modern algorithm
int c = std::count(begin(myVec), end(myVec), 5);
						



A brief recap/comparison of the two approaches:

    1. Cons:
  • Hand-made for loop
    (How many time did you mess up with loop indexes?)
  • Not immediately clear logic

    2. Pros:
  • Automatic loop unrolling
    (No more worries about indexes!)
  • Easy to understand logic


How does an algorithm work?

The algorithm is applied on a given container range, typically defined as: first, last.

According to the specific algorithm we are using we can have (or we can't have) several guarantees such as:
Modifying, Non-Modifying, Searching, Sorting...

Usually the pair begin(), end() iterators are used to define the sequence range, but there is no restriction set on the input range.


Iterators range. Any differences?



int c = std::count(begin(myVec), end(myVec), 5);
						


int c = std::count(myVec.begin(), myVec.end(), 5);
						


The first examplese uses free functions while the second uses std::vector member fucntions Note that the second example cannot work with C-style arrays, while the first is also able to accomodate them

Single responsability principle


Note that even if algorithms and lambdas are an extreme powerful resource it is always better not to increase the code complexity and readability creating extremely complicated and nested logics.


Function still serves as functions!



std::algorithm with a predicate


Example: std::find and std::find_if



// Finds a given elem into myVec
int a = std::find(myVec.begin(), myVec.end(), /*elem*/);

// Finds an element which satisfies a given predicate
int b = std::find_if(myVec.begin(), myVec.end(), /*predicate*/);
						


A predicate can be a so-called Functor* (before C++11) or a Lambda Function


Example: std::find_if with a predicate



int f = std::find_if(myVec.begin(), myVec.end(), 
    [](const Foo& foo){ return foo.value == 42; });
						


*A Functor, strictly speaking, is a class which overrides operator(), whose instances become callable objects.

More on predicates and iterators with std::algorithms



std::vector<std::string> strVec {"Not_Me", "Find_Me", "Not_You"};

int index = std::distance(strVec.begin(),
	std::find_if(strVec.begin(), strVec.end() 
	[](const std::string& s)
	{ return s.name == "Find_Me"; })
);
						


C++17 execution policies


  • std::execution::seq
  • std::execution::par
  • std::execution::par_unseq


Note that it is not possible to apply execution policies to all the std::algorithms.

Example:



std::vector<int> myVec { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// C++11 sequential sum using std::accumulate
// (can't be parallelized)
int sumSeq1 = std::accumulate(myVec.begin(), myVec.end(), 0); 

// Equivalent using new C++17 algorithm std::reduce()
int sumSeq2 = std::reduce(myVec.begin(), myVec.end(), 0);

// C++17 parallel sum
int sumPar = std::reduce(
	std::execution::par, myVec.begin(), myVec.end(), 0);
						

Lambda Functions


What is a Lambda Function?


A Lambda Function is an unnamed and callable object, able to capture variables from an enclosing scope.


Anatomy of a Lambda Function


[CaptureGroup](Parameters...) -> ReturnType { Body };
						

  1. Capture Group: Must always be present. See next slide for examples
  2. Body: logic of the lambda function. Can be empty
  3. (optional)Parameters: same as function parameters. Can be omitted if not needed
  4. (optional)Return Type: same as function return type. Can be automatically deduced


Capture Group

  • [ ]: empty capture list
    No local variables from the enclosing scope can be used in the lambda body
  • [&]: capture by reference
    All local variables from the enclosing scope are accessed by reference
  • [=]: capture by value
    All local variables from the enclosing scope are accessed by copy
  • [capture-list]: capture only specified variables
    Any combination of the previous is possible

Note that a local name preceded by & is always captured by reference and a local name not preceded by & is always captured by value

Capture Group example



int x = 4;
auto y = [&xRef = x, xCopy = x+1]() -> int 
{
    // xCopy is 5, global x is still 4
    xRef += 2; // global x incremented by 2
    return xCopy+2; // xCopy is incremented by 2
}(); 	
// x = 6
// y = 7
						


Anatomy of a std::function object



std::function<R(Params...)>
						


  • R: return type of the wrapped function.
  • (optional) Params: types of input parameters


Example: std::function declaration and usage


// wrap a free-function
void freeFunc(int i, std::string s, double d) { /*...*/ }; 
std::function<void(int, std::string, double)> myFunc 
    = freeFunc;

freeFunc(42, "hello world", 0.001); // invoke

// wrap a member function
class Foo { 
    void func(double d) { /*...*/ }; 
}
std::function<void(const Foo& foo, double)> myFooFunc 
    = &Foo::func();

myFooFunc(foo, 0.001); // invoke
						

std::function is a great replacement (more readable and typesafe) for pointers to function.

Function objects and Lambdas


Since std::function is a generic templated function wrapper can be used to store a Lambda Function as:


// Declaration:
std::function<void(int)> myPrint = [](int val) -> void 
{ std::cout << "Printed value: " << val; };

// Usage:
myPrint(42); // "Printed value: 42"
							


A more functional style


Lambdas and functions objects enables a more functional approach to the Modern C++:


Using these tools it is now possible to declare a function in a local scope (i.e. inside another function) and use it at any time in the enclosing scope


Functional example:


void Foo::myFunction(const std::vector<int>& myVec)
{
    /* ... */
    std::function<void(int)> myPrint = [](int val) -> void 
    { std::cout << "Printed value: " << val; };
    /* ... */
    for (auto e : myVec) myPrint(e);
}
						


Declare and invoke immediately


Lambdas can be declared and invoked directly in-place, after their declaration as:



// Declaration and immediate usage:
[](int val) -> void { 
    std::cout << "Printed value: " << val; 
}(42); // "Printed value: 42"
						


Embrace more complex const correctness



bool something = ...
const int myVal;
if (something) 
{
    myVal = 42;
} 
else 
{
    myVal = 0;
}
// This will not compile since myVal is
// declared const but not initialized
						


When the ternary operator (condition ? a : b) is not enough, lambdas are helpful:



bool something = ...
const int myVal = [something]() -> int 
{
    if (something) { return 42; } 
    return 0;
}();
						

The if-else logic is embedded in a lambda immediately invoked after its declaration. This allows to set the const int according to the desired condition.

auto keyword


auto ensures that the type of the variable that is being declared will be automatically deduced from its initializer.

auto takes exactly the type on the right-hand side removing const/volatile and &/&&.


auto  foo1 = new Foo();
auto* foo2 = new Foo(); // equivalent, * is kept

auto  bar1 = getBar();
auto& bar2 = getBar(); // different, & is removed
						


Can't forget variable initialization



Foo foo; // OK, but foo might not be initialized
auto bar; // Does not compile because initialization is missing

auto bar = Bar{ }; // OK, bar is initialized
						


Guarantees that no implicit conversions occur (including narrowing conversions)



int val = 42.f; 
// OK, but float to int narrowing conversion kicks in
// Is that desired or is it a bug? 
// If it is really desired why is it done like that?

auto val = 42.f; // OK
						


Avoid unnecessary temporary conversions



std::string getValue() { return "42"; } 
// const char* to std::string conversion (1)

MyString val = getValue(); 
// std::string to MyString conversion (2)

auto val = getValue(); // OK
						

Note that a temporary can be created during the assignment if implicit conversion is disabled. In the first case we can have two conversions Using auto two conversions are saved

Keeps on using the correct type even if initialization is modified

Suppose that the previous getValue() function is changed as:


// std::string getValue() { return "42"; } // previous
int getValue() { return 42; } // new
std::string val = getValue(); // usage is unchanged
// Does not compile anymore after 
// getValue() signature has changed

auto val = getValue(); // OK
// Keeps on working without changing client code
						

Changing a function signature may result in non compilable code

Hard to catch narrowing conversion



std::vector<int> myVec { /* ... */ };
for (int i = 0; i < myVec.size(); ++i){ /* ... */ }
// OK, but can easily overflow. Why?

// std::vector::size() returns size_t (can be 4 bytes), 
// not an int (can be 2 bytes)

for (auto i = 0; i < myVec.size(); ++i){ /* ... */ } // OK
						


More on narrowing conversion
(unexpected side effects)



std::vector<float> myVec { 1.0, 2.0, 3.0 };

auto sumA = std::accumulate(myVec.begin(), myVec.end(), 0.0); 
// OK, sumA is float as expected.

auto sumB = std::accumulate(myVec.begin(), myVec.end(), 0);  
// OK, BUT sumB is int because the type of "0" 
// in std::accumulate is set as the return type
						


Smart pointers "Heap-like" initialization



std::unique_ptr<Bar> b 
	= std::unique_ptr<Bar>(new Bar()); // OK   
	
auto f = make_unique<Foo>(); // OK, better
						


Use of literal suffixes for uniform initialization



int x = 42;                       auto x = 42;
float x = 42.;                    auto x = 42.f;
unsigned long x = 42;             auto x = 42ul;

using namespace std::string_literals; // C++14
std::string x = "42";             auto x = "42"s;   

using namespace std::chrono_literals; // C++14
std::chrono::nanoseconds x{ 42 }; auto x = 42ns;    
						


Template function with trailing return type



template <class T, class U>
auto getValue(const T& t, const U& u) -> decltype(T + U){
    return t + u;
}
						

Provides great flexibility for generic templated code

Member function with trailing return type



class Foo {
    /* ... */
    std::map<const std::string, std::shared_ptr<Bar>> mFooMap;
}

auto Foo::func(const std::string& id)
 -> decltype(mFooMap)::iterator 
{
    return std::find_if(mFooMap.begin(), mFooMap.end(),
        [id](const auto& pair) {return id == pair.first.name; });
}
						

If mFooMap is changed, Foo::func() and all its usages don't require any changes

And finally, less typing for the lazy ones



std::map<Foo::Bar, std::vector<T>>::const_iterator it 
    = myMap.begin(); // OK
	
auto it = myMap.begin(); // OK, lazy
						

Thank you!

What's next?


C++17 new features

Questions?