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:
[capture_specification] (function_parameters) -> return_type { body }
Compare that to the syntax for the definition of a normal function:
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.
#include <iostream>
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:
#include <numeric>
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.
#include <iostream>
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
#include <iostream>
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
#include <iostream>
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
#include <iostream>
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
#include <iostream>
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.
#include <functional>
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<void(void)> 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:
...
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 <algorithm>
header.
For example, finding a struct with a particular field in a list:
#include <algorithm>
#include <array>
#include <vector>
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 <algorithm> 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 <typename T>
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_struct_t, 1000> my_array;
extern std::vector<my_struct_t> 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:
#include <cstdio>
#include <functional>
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<void(size_t, size_t)> 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.