Everything you need to know about Lambdas, avoiding 'catch', and safe string-views in 75 mins or Less!

Erich Keane, iCDG Software Engineer

Erich.Keane@intel.com

-Lambdas
-Declarative Control Flow
-string_view

Lambda Functions

  • Anonymous (unnamed) functions
  • Invented in 1930s (Alonzo Church)
  • First used in Lisp (1958), foundation of Functional Languages (Haskell, Erlang, etc)
  • Since added to most imperative languages due to their usefulness
  • Added to C++ as of C++11, given generics in C++14
  • Incredibly useful for functions that don't NEED a name (only referred to 1x), and are used for a short-time

Lambdas in C++

  • First Class Functions
    • same efficiency as a Function Object
    • In fact, implemented as Function Objects
    • Full optimization
  • Implicitly convertible to Function Pointers
    • Useful for any function that takes a Predicate
    • REALLY useful for function composition
    • Usable with C function pointers
  • Better replacement to std::bind (does partial application)
    • ALWAYS compile-time typed
    • Optimized aggressively
    • Explicit copy/reference semantics
    • Nicer Error Messages
    • Can have state via statics

Lambda Syntax

4 Parts: []()->retType{}

  1. Capture []
    • List of out-of-scope variables to gain access to
    • Permits aliasing/state assignment in C++14
  2. Parameters List ()
    • List of parameters (part of signature)
    • Can use auto in C++14
    • Optional, not required if there are no parameters!
    • Return type not specified, it is deduced!
  3. Return Type: Almost never specified, type auto-deduces
  4. Block for Execution {}
    • Any C++ compatible code that you'd want to use!  Has access to Globals, local-statics, captures, and parameters

Lambda Syntax

Ways to Capture:

  • Nothing: [](){}
  • Everything as a copy: [=](){}
  • Everything as a reference: [&](){}
  • Single thing by copy: [varName](){}
  • Single thing by reference:[&varName](){}
  • Multiple things: [var1, &var2, var3](){}
  • Everything but 1 by copy: [=,&refVar](){}
  • Current object: [this](){}
  • Rename (C++14): [v = varName](){}
  • Init Value (C++14): [v = 12](){}
    • Useful for calls to aggregate

Lambdas are Function Objects!

// Function object with state:
template<typename paramType>
class IsNotEqualFinder {
    const paramType& orig;
    public:
    IsNotEqualFinder(const paramType& o):
    orig(o){}
    
    bool operator()(const paramType& pt){
        return orig != pt;
    }
};

Foo original;
std::vector<Foo> list = AllFoos();
IsNotEqualFinder<Foo> finder(original);

std::vector<Foo> listWoFoo;
std::copy_if(list.begin(), list.end(),
               listWoFoo.begin(), finder);     
//C++14 Only!
Foo original;
std::vector<Foo> list = AllFoos();
auto finder = [&orig = original]
                (auto pt){ return orig != pt;};
                
std::vector<Foo> listWoFoo;
std::copy_if(list.begin(), list.end(),
               listWoFoo.begin(), finder);     

//C++11:

Foo original;
std::vector<Foo> list = AllFoos();
auto finder = [&original]
                (const Foo& pt)
                { return original != pt;};
                
std::vector<Foo> listWoFoo;
std::copy_if(list.begin(), list.end(),
               listWoFoo.begin(), finder);

Lambdas vs Functions

  • Lambdas just as fast
    • Terse sytnax!
  • Use sparingly as a replacement, usually for Predicates/small functions that aren't reused elsewhere
  • Lambdas can have state
  • Otherwise identical capabilities
// Function mechanism, used only 1x,
// not necessarily nearby!
void DeleteThing(ThingType* thing){
    delete thing;
}

// Calling site:
C_Call_With_Callback(myThing, &DeleteThing);

// Lambda Calling site:
C_Call_With_Callback(myThing, [](void* t){delete thing;});

// C++14 Lambda Calling Site:
C_Call_With_Callback(myThing, [](auto t){delete thing;});

Lambdas vs Function Objects

struct CompareThings
{
    CompareAndCount() = default;
    
    bool operator()(const Thing& t1, const Thing& t2){
        return t1 < t2;
    }
};

std::vector<Thing> myThings = getThings();
CompareThings ctr;
std::sort(myThings.begin(), myThings.end(), ctr);

std::vector<Thing> myThings = getThings();
auto ctr = [](const auto& t1, const auto& t2){return t1 < t2;};
std::sort(myThings.begin(), myThings.end(), ctr);

Lambdas Instead of Bind

// Callback we want to use, note the context
struct OtherPart{
void CBHandler(void* context, char type, const Thing& stuff);};

// But call takes a function!
typedef void (*cb_func)(const Thing&/* stuff*/, char /*type*/, bool /*unused*/);
void StartAsyncThing(cb_func pFunc);

// Using Bind.  harder to read, type erasure, everything captured by 
// copy unless specified otherwise
StartAsyncThing(std::function<void(const Thing&, char, bool)>(
    std::bind(&OtherPart::CBHandler, opPtr, addedContext, _2, _1)
    )));

// Using Lambda:
StartAsyncThing(
        [opPtr, addedContext](const Thing& stuff, char type, bool)
            {    opPtr->CBHandler(addedContext, type, stuff);    }
    );

// Using Lambda, C++14!
StartAsyncThing(
        [opPtr, addedContext](auto stuff, auto type, auto b)
            {    opPtr->CBHandler(addedContext, type, stuff);    }
    );

Declarative Control Flow

  • try/catch blocks, and nested try/catch blocks get really bad
  • try/catch blocks are tough to ensure they get properly unwound
  • without a 'finally' (not in C++) try/catch blocks require repetition to cleanup on error

What can we do?

  • Take advantage of RAII constructs to do cleanup for us!
    • Create 3 types to handle the 3 different conditions for cleanup
      • scope_exit // define what happens ALWAYS
        • Not terribly necessary, since normal RAII containers can replace this
        • alternatives: string/vector/smart-ptrs
      • scope_success // define what happens on success
      • scope_fail // define what happens when an exceptino is thrown
      • Sadly, scope_success/scope_fail tougher in C++11/14
        • Very easy in C++17 with int std::uncaught_exceptions instead of bool std::uncaught_exception

Example

// Instead of:
template<typename Fn, size_t N>
void write_file(const path& name, 
    Fn writeFunc, array<uint8_t, N> data)
{
    create_file(name);
    try
    {
        writeFunc(data);
        save_and_close(name);
        log("write file done");
    }
    catch(...)
    {
        delete_file(name);
        log("write file done");
        throw;
    }
}
// This!
template<typename Fn, size_t N>
void write_file(const path& name, 
    Fn writeFunc, array<uint8_t, N> data)
{
    scope_exit exGuard([]
        {log("write file done");});
    create_file(name);
    scope_fail failGuard([&name]
        {delete_file(name);});
    scope_success successGuard([&name]
        {save_and_close(name);});

    writeFunc(data);
}

Simple Implementation

class scope_exit{
    private:
    (void*)() func;
    public:
    scope_exit((void*)() f) : func(f){}
    ~scope_exit(){func();}
};
class scope_fail{    
    private:
    (void*)() func;
    int count;
    public:
    scope_fail((void*)() f) : func(f), count(uncaught_exceptions()){}
    ~scope_fail(){
        if (count != uncaught_exceptions()) func();
    }
};
class scope_success{   
    private:
    (void*)() func;
    int count;
    public:
    scope_success((void*)() f) : func(f), count(uncaught_exceptions()){}
    ~scope_success(){
        if (count == uncaught_exceptions()) func();
    }
};

string_view

  • C++17, named std::experimental::string_view
    • Also boost::string_ref
  • Describes an object that refers to a constant, contiguous sequence of char-like objects
  • Provides a "view" into any type of string
  • Non-owning, lifetime management up to user
  • Almost the same interface as a constant std::string
  • Implemented simply as a pointer and a length
  • When to use?
    • Passing as a parameter to a 'pure' function (one that doesn't modify the parameter)
    • Returning a reference from a function
    • A reference to a part of a long-lived structure

string_view legacy calls

// Original
void legacy_call(const char *param){...}
legacy_call("Thing being Frobbled");
string foo;
legacy_call(foo.c_str());

Partial conversion is less efficient:
void legacy_call(const string& param){...}
legacy_call("Thing being Frobbled");
string foo;
legacy_call(foo.c_str()); // results in an extra std::temporary

OR!:
void legacy_call(string_view param){...}
legacy_call("Thing being Frobbled");
string foo;
// identical, except size only calculated 1x
legacy_call(foo.c_str()); 
// No extra copy, size is now consistent in legacy call!
legacy_call(foo);

string_view construction

// std::string:
std::string other;
string_view view(other);

// Pointer + length:
void foo(char* ptr, int size)
{
    string_view myView {{ptr, size}};
}

// Null terminated string (requires traversal)
char* other;
string_view view(other);

string_view and string

// Compares to a std::string:
string_view a;
string b;
if (a == b){...}

// Compares to a null terminated string
if (a == "null term string"){...}
char* c = "null term string";
if (a == c){...}

// converts to a string:
string b = view.to_string();

// good 'find' methods:
find // find first character/substring
rfind // find last character/substring
find_first_of/find_last_of/find_first_not_of/find_last_not_of

// Additional good functions
operator[], at
front/back
size/length
empty
begin/end/cbegin/cend/rbegin/rend/crbegin/crend
data()
remove_prefix // shrink view by advancing ptr
remove_suffix // shrink view by shrinking length
substr // gets string_view to a smaller section

string_view drawbacks

  • Not null terminated
  • Lifetime externally managed
  • Only C++17, though boost::string_ref does same things!

string_view usage example

#include <iostream>
using std::string;
using std::cout;
using std::endl;

string extractB(const string& o)
{
    auto wobraces = o.substr(1, o.size() -2);
    auto justB = wobraces.substr(
                    wobraces.find('b') - 1);
    auto justVal = justB.substr(
                    justB.find(':') + 1);
    auto noQuotes = justVal.substr(
                    1, justVal.size() - 2);
    return noQuotes;
}

int main()
{
    string orig = "{'a':'aval','b':'bval'}";
    string after = extractB(orig);

    cout << after <<endl;
}

string_view usage example

#include <iostream>
using std::string;
using std::cout;
using std::endl;

string extractB(const string& o)
{
    auto wobraces = o.substr(1, o.size() -2);
    auto justB = wobraces.substr(
                    wobraces.find('b') - 1);
    auto justVal = justB.substr(
                    justB.find(':') + 1);
    auto noQuotes = justVal.substr(
                    1, justVal.size() - 2);
    return noQuotes;
}

int main()
{
    string orig = "{'a':'aval','b':'bval'}";
    string after = extractB(orig);

    cout << after <<endl;
}
#include <iostream>
#include <experimental/string_view>
using std::string;
using std::cout;
using std::endl;
using std::experimental::string_view;

string extractB(string_view o)
{
    auto wobraces = o.substr(1, o.size() -2);
    auto justB = wobraces.substr(
                    wobraces.find('b') - 1);
    auto justVal = justB.substr(
                    justB.find(':') + 1);
    auto noQuotes = justVal.substr(
                    1, justVal.size() - 2);
    return noQuotes;
}

int main()
{
    string orig = "{'a':'aval','b':'bval'}";
    string after = extractB(orig);

    cout << after <<endl;
}

Q&A

Erich.Keane@intel.com

Made with Slides.com