C++14 is available for the following TI compiler versions, beginning with the
indicated release version:
| Compiler ISA | Version |
|--------------------|------------|
| TI Proprietary Arm | 18.1.1.LTS |
| TI Arm Clang | 1.3.0.LTS |
| MSP430 | 18.1.1.LTS |
| C6000 | 8.3.0 |
This series of articles serves to introduce various new features in the
language, as well as to provide small examples of how the features might apply
to embedded application development.
## Lambda Expressions
Lambda expressions in C++14 are functions that can be treated as any other
object, such as a class or struct. They can utilize variables defined in
the same scope, 'capturing' them implicitly or explicitly by value or
reference.
A lambda object (also known as 'closure object') can be called like a normal
function. Similar to function pointers, they can be passed as an argument to
another function and be called from that context as well.
### Syntax
The basic syntax for a **lambda expression** is as follows:
```cpp
[capture_specification] (function_parameters) -> return_type { body }
```
Compare that to the syntax for the definition of a normal function:
```cpp
return_type name(function_parameters) { body }
```
Note the positioning of **return_type** and the absence of **name** in the
lambda syntax.
The return type for a lambda is specified using a C++ feature named
'trailing return type'. This specification is optional. Without the trailing
return type, the return type of the underlying function is effectively 'auto',
and it is deduced from the type of the expressions in the body's return
statements.
The name is effectively the name of a variable to which a **lambda expression**
is assigned. The 'auto' type inference keyword must be used because the type
of a lambda object is both anonymous and internal.
Here's a basic example, utilizing lambdas to create a function that squares an
integer.
```cpp
#include
int main()
{
auto square = [] (int num) { return num * num; };
std::cout << square(2) << std::endl; // 4
std::cout << square(25) << std::endl; // 625
std::cout << square(17) << std::endl; // 289
}
```
One interesting construct is a function that returns an unnamed lambda object.
This is similar to decorators in python, which are functions which return
functions:
```cpp
#include
auto get_accumulator() {
return [] (int a, int b) { return a + (b * b); };
}
int sum_of_squares(int integers[], int count) {
return std::accumulate(
&integers[0], &integers[count], 0,
get_accumulator()
);
}
```
#### Parameters
Parameters for a lambda are similar to those of normal funtions, with one
important addition: The 'auto' type specifier can be used. Parameters declared
with 'auto' in a lamda's parameter list have their type deduced, and a
specialization of the lambda is generated to handle it.
```cpp
#include
struct complex_float {
complex_float(float r, float i) : real(r), imag(i) { }
complex_float operator*(const complex_float &rhs) {
return complex_float(real * rhs.real - imag * rhs.imag,
real * rhs.imag + imag * rhs.real);
}
float real;
float imag;
};
int main() {
auto square = [] (auto num) { return num * num; };
// num deduced as int
std::cout << square(2) << std::endl; // 4
// num deduced as double
std::cout << square(2.0) << std::endl; // 4.0
// num deduced as complex_float
complex_float result = square(complex_float(2, 1));
std::cout << "(" << result.real << ", "
<< result.imag << "i)" << std::endl; // (3, 4i)
}
```
#### Captures
One truly unique mechanic of lambdas that normal functions can't replicate
is the ability to capture and utilize variables from the current scope.
While this is a powerful feature, it also comes with the caveat that capturing
by reference or capturing pointers runs the risk of invoking undefined
behavior
The capture specification can imply implict or explicit and, like passing
arguments as function parameters, variables can be captured in two different
ways: by value or by reference. These lambdas can then be transferred just like
any other lambda to separate scopes to be called, and will maintain the
captured variable.
The following are not examples that should be used in real code, but should
serve to help understand the general syntax and behavior of the capture:
* By value, explicit
```cpp
#include
int main() {
int result;
int val = 1;
result = [val] () { // Capture val, explicitly
// ERROR: Can't modify by-value captures
// val += 1;
return val + 1;
}();
std::cout << "By value, explicit:";
std::cout << " returned " << result; // 2
std::cout << " value after " << val; // 1
std::cout << std::endl;
return 0;
}
```
* By value, implicit
```cpp
#include
int main() {
int result;
int val = 1;
result = [=] () { // Capture all variables used in the
// lambda by value
// ERROR: Can't modify by-value captures
// val += 1;
return val + 1;
}();
std::cout << "By value, implicit:";
std::cout << " returned " << result; // 2
std::cout << " value after " << val; // 1
std::cout << std::endl;
return 0;
}
```
* By reference, explicit
```cpp
#include
int main() {
int result;
int val = 1;
result = [&val] () { // Capture val by reference
val += 1; // Modify val in the original scope
return val + 1;
}();
std::cout << "By reference, explicit:";
std::cout << " returned " << result; // 3
std::cout << " value after " << val; // 2
std::cout << std::endl;
return 0;
}
```
* By reference, implicit
```cpp
#include
int main() {
int result;
int val = 1;
result = [&] () { // Capture all variables used in the
// lambda by reference
val += 1; // Modify val in the original scope
return val + 1;
}();
std::cout << "By reference, implicit:";
std::cout << " returned " << result; // 3
std::cout << " value after " << val; // 2
std::cout << std::endl;
return 0;
}
```
Care must be taken when using captures in class/struct member functions.
Code that looks like it captures a variable might just be capturing the
implicit 'this' pointer each nonstatic member function has. When this occurs,
the chances of unintentionally causing undefined behavior greatly increases.
```cpp
#include
extern bool has_peripheral(int);
class BitTogglerGenerator {
public:
BitTogglerGenerator(volatile char *bits) : _bits(bits) {}
auto getToggleFn() {
return [=] () {
// _bits is NOT a local variable. The 'this' pointer for
// getToggleFn is, however. Thus, we're capturing 'this' by
// value, and _bits here is implicitly this->_bits
char tmp = ~(*_bits);
*_bits = tmp;
};
}
public:
volatile char *_bits;
};
extern volatile char MAPPED_REG;
int main() {
std::function toggle_fn = [] () {};
if (has_peripheral(0)) {
BitTogglerGenerator gen(&MAPPED_REG);
toggle_fn = gen.getToggleFn(); // Toggle MAPPED_REG
}
// At this point, gen has been destroyed, and the 'this' pointer capture
// in the toggle_fn is in an undefined state.
for (int i = 0; i < 1000; i++) {
toggle_fn(); // Undefined behavior, null-pointer dereference
}
return 0;
}
```
The correct way to do the capture above is to explicitly capture _bits, and
use the C++14 method of initializing a capture:
```cpp
...
return [_bits=_bits] () {
char tmp = ~(*_bits);
*_bits = tmp;
}
...
```
In this way, the 'this' pointer is not captured, only the value of the
object's _bits member.
### Library Support
Lambdas are helpful for using various standard library functions which expect
a function pointer, such as those found in the `` header.
For example, finding a struct with a particular field in a list:
```cpp
#include
#include
#include
typedef struct {
int data;
int id;
} my_struct_t;
// In C
static my_struct_t *find_id_c(int id, my_struct_t *object_list,
size_t length) {
my_struct_t *found = NULL;
size_t i;
for (i = 0; i < length; i++) {
if (object_list[i].id == id) {
found = &object_list[i];
break;
}
}
return found;
}
// In C++, using C-styled arrays
static my_struct_t *find_id_cpp14(int id, my_struct_t *object_list,
size_t length) {
// std::find_if, and many other header functions, accept a
// function-like object. Lambdas are such objects. In this case,
// std::find_if wants a function which takes a single object and returns
// a boolean determining if the object fits into the desired criteria.
my_struct_t *found = std::find_if(
&object_list[0], &object_list[length],
[id] (const my_struct_t &object) { return object.id == id; }
);
return (found == &object_list[length]) ? NULL : found;
}
// In C++, accepts any iterable container of my_struct_t objects
template
static my_struct_t *find_id_cpp14_generic(int id, T &object_list) {
// As above, except note that we do not have the parameter 'length'. We've
// replaced that parameter with the std::begin and std::end helpers which
// understand how to find the start and end of a container object.
auto found = std::find_if(
std::begin(object_list), std::end(object_list),
[id] (const my_struct_t &object) { return object.id == id; }
);
return (found == std::end(object_list)) ? NULL : &(*found);
}
extern my_struct_t my_c_array[1000];
extern std::array my_array;
extern std::vector my_vector;
extern void use_object(my_struct_t *);
void try_c() {
my_struct_t *found_c = find_id_c(42, my_c_array, 10);
use_object(found_c);
}
void try_cpp14() {
my_struct_t *found_cpp14 = find_id_cpp14(42, my_c_array, 10);
use_object(found_cpp14);
}
void try_generic_c_array() {
my_struct_t *found_generic = find_id_cpp14_generic(42, my_c_array);
use_object(found_generic);
}
void try_generic_cpp_array() {
my_struct_t *found_generic = find_id_cpp14_generic(42, my_array);
use_object(found_generic);
}
void try_generic_cpp_vector() {
// Beware, adding to/removing from my_vector might invalidate this
// pointer!
my_struct_t *found_generic = find_id_cpp14_generic(42, my_vector);
use_object(found_generic);
}
```
### Embedded Use Cases
Generally, lambdas are good for consolidating blocks of code which perform
similar tasks on different, but commonly typed inputs. This tends to happen
quite often in the embedded programming space, particularly ones performing
some level of mathematical calculations on input.
Furthermore, lambdas can capture particular peripherals or data structures in
and send it to a generic handler:
```cpp
#include
#include
extern bool predicate1();
extern bool predicate2();
// std::function is required because lambdas that capture can't be converted
// into function pointers.
extern void another_setup_routine(std::function setter);
void setup_register(volatile int *reg) {
// Default arguments are allowed for lambdas as well
auto set_bits = [reg] (size_t start, size_t len=1) {
size_t mask = (((1 << len) - 1) << start);
*reg = (*reg | mask);
};
if (predicate1()) set_bits(3, 2);
if (predicate2()) set_bits(25);
another_setup_routine(set_bits);
}
```
### Performance
With normal optimization, most simple lambdas will be inlined. For example,
the 'setup_register' code sample above will inline constant '|' operations
for calls to set_bits. The 'find_if' examples above inline down to a
simple loop in assembly, and does not contain any calls besides the helper
'use_object'.
Lambda objects do not have hidden memory management like many other standard
C++ objects. However, the std::function objects seen commonly beside lambdas
sometimes cause memory allocations. If this is a problem for your application,
then avoid std::function entirely. A lambda that captures can incur memory
overhead when variables are captured by-value. Avoid capturing large
objects by value, and avoid capturing altogether if memory is at a premium.
### More Information
* [The CPPReference Page for **lambda expressions**]( https://en.cppreference.com/w/cpp/language/lambda "lambda expressions at cppreference.com" )