C++ Basic Guidelines for EOS Development: Iterators and Lambda Expressions

Keywords: Blockchain Lambda Javascript Programming

Let's talk about iterators, which are a very useful tool and are widely used throughout the EOS code base. If you come from a JavaScript background, you may already be familiar with iterators, just as they are used for loops. The key concept of iterators is to provide a better way to traverse item sets. The additional benefit is that you can implement an iterator interface for any custom class, making iterators a common way to traverse data.

// @url: https://repl.it/@MrToph/CPPBasics-Iterators
#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> v{2, 3, 5, 8};
  // old way to iterate
  for (int i = 0; i < v.size(); i++)
  {
    cout << v[i] << "\n";
  }

  // using Iterators
  // begin() returns an iterator that points to the beginning of the vector
  // end() points to the end, can be compared using != operator
  // iterators are incremented by using the + operator thanks to operator-overloading
  for (vector<int>::iterator i = v.begin(); i != v.end(); i++)
  {
    // iterators are dereferenced by * like pointers
    // returns the element the iterator is currently pointing to
    cout << *i << "\n";
  }

  // auto keyword allows you to not write the type yourself
  // instead C++ infers it from the return type of v.begin
  for (auto i = v.begin(); i != v.end(); i++)
  {
    cout << *i << "\n";
  }

  // can use arithmetic to "jump" to certain elements
  int thirdElement = *(v.begin() + 2);
  cout << "Third: " << thirdElement << "\n";
  // end is the iterator that points to the "past-the-end" element
  // The past-the-end element is the theoretical element that would follow the last element in the vector.
  // It does not point to any element, and thus shall not be dereferenced.
  int lastElement = *(v.end() - 1);
  cout << "Last: " << lastElement << "\n";

  // do not go out of bounds by iterating past the end() iterator
  // the behavior is undefined
  // BAD: v.end() + 1, v.begin() + 10
}

In modern C++, iterators are the preferred way to iterate the set of elements (vectors, lists, mappings). In addition, auto keywords can avoid entering word types, but may cause code performance degradation.

Lambda expression

Using iterators, we can begin to study the concept of functional programming in modern C++. Many functions in the standard library take a series of elements represented by two iterators (start and end) and anonymous functions (lambda functions) as parameters. This anonymous function is then applied to each element in the scope. They are called anonymous functions because they are anonymous. E not bound to variables, but they are short logical blocks that are passed as inline parameters to higher-order functions. Usually, they are unique to the functions passed to them, so there is no need for the entire overhead of having a name (anonymity).

With it, we can implement constructs like sorting, mapping, filtering, etc., which are easy to implement in languages such as JavaScript:

[1,2,3,4].map(x => x*x).filter(x => x % 2 === 1).sort((a,b) => b - a)

The code in C++ is not concise, but it has the same structure. Many functional programming assistants from the std library run at half-spaced intervals, which means that they include lower ranges and exclude higher ranges.

// @url: https://repl.it/@MrToph/CPPBasics-Lambdas
#include <iostream>
#include <vector>
// for sort, map, etc.
#include <algorithm>

using namespace std;

int main()
{
  vector<int> v{2, 1, 4, 3, 6, 5};
  // first two arguments are the range
  // v.begin() is included up until v.end() (excluded)
  // sorts ascending
  sort(v.begin(), v.end());

  // in C++, functions like sort mutate the container (in contrast to immutability and returning new arrays in other languages)
  for (auto i = v.begin(); i != v.end(); i++)
  {
    cout << *i << "\n";
  }

  // sort it again in descending order
  // third argument is a lambda function which is used as the comparison for the sort
  sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

  // functional for_each, can also use auto for type
  for_each(v.begin(), v.end(), [](int a) { cout << a << "\n"; });

  vector<string> names{"Alice", "Bob", "Eve"};
  vector<string> greetings(names.size());

  // transform is like a map in JavaScript
  // it applies a function to each element of a container
  // and writes the result to (possibly the same) container
  // first two arguments are range to iterate over
  // third argument is the beginning of where to write to
  transform(names.begin(), names.end(), greetings.begin(), [](const string &name) {
    return "Hello " + name + "\n";
  });
  // filter greetings by length of greeting
  auto new_end = std::remove_if(greetings.begin(), greetings.end(), [](const string &g) {
    return g.size() > 10;
  });
  // iterate up to the new filtered length
  for_each(greetings.begin(), new_end, [](const string &g) { cout << g; });
  // alternatively, really erase the filtered out elements from vector
  // so greetings.end() is the same as new_end
  // greetings.erase(new_end, greetings.end());

  // let's find Bob
  string search_name = "Bob";
  // we can use the search_name variable defined outside of the lambda scope
  // notice the [&] instead of [] which means that we want to do "variable capturing"
  // i.e. make all local variables available to use in the lambda function
  auto bob = find_if(names.begin(), names.end(), [&](const string &name) {
    return name == search_name;
  });
  // find_if returns an iterator referncing the found object or the past-the-end iterator if nothing was found
  if (bob != names.end())
    cout << "Found name " << *bob << "\n";
}

The grammar of anonymous functions is something that is customary in C++. They are specified in parentheses, followed by a list of parameters such as [] (int a, int b) - > bool {return a > b;}. Note that - > bool specifies a Boolean return value. Usually, you can avoid expressing return types, because it can be inferred from the return types in the body of the function.

If you want to use variables defined in scopes other than lambda functions, you need to capture variables. It is also possible to pass parameters to your function by reference or value.

  • To pass by reference, you need to start lambda with the [&] character (just like when using a reference in a function): [&]
  • To pass values, use the = character: [=]

It is also possible to mix and match capture by value and reference.

For example, [=, & foo] will create copies of all variables except foo, which are captured by reference.

It helps to understand what happens behind the scenes when lambdas are used:

It turns out that lambdas is implemented by creating a small class that overloads operator(), so it acts like a function. The lambda function is an instance of this class; when constructing a class, any variable in the surrounding environment is passed to the lambda function class's constructor and saved as a member variable. In fact, it's a bit like the idea of imitating functions that are already possible. The advantage of C++ 11 is that it's very simple to do so -- so it makes sense that you can always use it instead of just writing a new class in very rare cases.

Lambda functions are widely used in EOS smart contracts because they provide a very convenient way to modify data in a small amount of code. There are more functions in the standard library, which are similar to those we see in sort, transform, remove_if and find_if. They are all exported through the < algorithm > header.

Posted by Garth Farley on Thu, 25 Apr 2019 12:27:36 -0700