Sunday, February 15, 2015

A Few Great C++11 Features

A few years back, I heard a talk by John Spicer of Edison Design Group on new features in C++11. After hearing John's talk, I realized that my students and I all fell into his category of programmers who knew the "FORTRAN subset of C++".  Sure, I've written lots of code over the years, and a C++ compiler will understand that code.  But was it really C++?  Was it idiomatic?  And even if it was, would it still be considered idiomatic in the face of C++11?

C++11 adds tons of cool features.  The concurrency support is great.  The STL is even better than before.  But the best part, in my opinion, is lambdas.  With lambdas, data structure design gets a lot easier.  One "map" function can replace all of those one-off functions that otherwise go into a data structure (min, max, average, selectIf, countIf, etc.).

Here's a program I wrote to demonstrate some of the features of C++11.


/// This is a demonstration of a few nice C++11 features: auto, the ':'
/// iterator, initializer lists, std::function, and lambda expressions.  In
/// many cases, it's possible to achieve the same effect as I present using
/// even less code, but by being explicit I hope I have made the code and
/// comments easy to follow

#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std; // I'm being lazy here... should explicitly use cout,
                     // endl, function, map, pair, string, and vector


/// wrapped_map_t extends map<string, string>.  We start with a map of
/// key/value pairs, where both the key and value are strings, and we add a
/// new method to it.  The new method, apply_to_all, lets us apply a lambda
/// to (a copy of) every key/value pair that is in the map.
///
/// my claim is that lambdas fundamentally change the art of data structure
/// design.  To support my claim, I will show how this one simple function
/// obviates many public functions that one might want int a data structure
/// (count, print, extract_keys, find_matching_keys, etc).
struct wrapped_map_t : public map<string, string>
{
    /// the apply_to_all function simply iterates through the key/value pairs
    /// of the map, calling lambda(key, value) for each pair.  Strictly
    /// speaking, std::map is a red/black tree, and this will do a pre-order
    /// traversal
    void apply_to_all(function<void(const string, const string)>& lambda)
    {
        // note the use of C++11 'auto' keyword to avoid having to specify
        // the type of the iterator
        for (auto i = begin(), e = end(); i != e; ++i)
            lambda(i->first, i->second);
    }
};

int main()
{
    // declare an instance of the extended map
    wrapped_map_t kvpairs;

    // populate the map with some keys and values.  We're going to use some
    // great C++ magic here: initializer lists.  We don't even have to know
    // the type of the list, but the compiler will deduce that it's something
    // like pair<string,string>[]
    kvpairs.insert({{"donald", "duck"}, {"mickey", "mouse"},
                    {"minnie", "mouse"}, {"bat", "man"},
                    {"super", "man"}, {"milo", "otis"},
                    {"mighty", "mouse"}, {"rocky", "squirrel"},
                    {"bullwinkle", "moose"}});

    // 'func' is a reference to a function that takes two strings as its
    // parameters, and returns nothing.  This is a lot nicer than the old C
    // syntax in use before C++11 (i.e., void (*func)(string, string)).
    function<void(string, string)> func;

    // let's assign a new lambda to func.  the lambda will simply print its
    // parameters
    func = [](string k, string v) { cout << "{" << k << ", " << v << "} "; };
    // now let's apply func to every element in the set.  No need for a
    // "print_in_sorted_key_order" function in our class.  It would be
    // trivial to add "print keys" and "print values" features without
    // changing the data structure... we'd just need to pass different
    // lambdas
    cout << "KVPairs = ";
    kvpairs.apply_to_all(func);
    cout << endl;

    // The last example was pretty boring.  A function pointer could have
    // done that easily.  Now let's pass in a function that has a captured
    // reference to a variable that is local to this function.  In that way,
    // each time the lambda is called, it can access the function 'my_count'
    // that we declare *right here*.  The net result is that we can count the
    // elements in the map, without requiring map to provide a "count()"
    // function.  Note that the parameters to the lambda aren't used, and
    // that's OK.
    int my_count = 0;
    func = [&my_count](string k, string v) { my_count++; };
    kvpairs.apply_to_all(func);
    cout << "Elements in map = " << my_count << endl;

    // OK, so we could have achieved that effect with an apply_to_all that
    // took three parameters: string, string, int&.  But would we really want
    // to do that for every possible pass-by-reference type?  For example,
    // here we pass a vector of strings so that we can copy out all keys from
    // the map
    vector<string> keys;
    func = [&keys](string k, string v) {keys.push_back(k);};
    kvpairs.apply_to_all(func);
    // print the keys, to show that it worked
    cout << "Keys in map = {";
    for (auto i : keys)
        cout << i << ",";
    cout << "}" << endl;

    // note, too, that we can use the lambda to capture multiple local
    // variables, and that some can be pass by value while others are pass by
    // reference.  Here 'pre' is pass by value, 'keys' is pass by reference,
    // and our function copies into 'keys' all keys in the map whose values
    // begin with 'mo'
    string pre = "mo";
    keys.clear();
    func = [&keys, pre](string k, string v) { if (v.find(pre) == 0) keys.push_back(k);};
    kvpairs.apply_to_all(func);
    cout << "Keys whose values start with 'mo' = {";
    for (auto i : keys)
        cout << i << ",";
    cout << "}" << endl;

    // this example is much like the prior one.  Here we extract all
    // key/value pairs where the key begins with 'mi'
    pre = "mi";
    vector<pair<string, string>> pairs;
    func = [&pairs, pre] (string k, string v)
        { if (k.find(pre) == 0) pairs.push_back(make_pair(k, v)); };
    kvpairs.apply_to_all(func);
    cout << "Elements whose key begins with 'mi' = {";
    for (auto i : pairs)
        cout << "{"<<i.first<<", "<<i.second<<"} ";
    cout << "}" << endl;

    // the point of this example is to illustrate that when the lambda is
    // called with the key and value, the function we are executing *cannot*
    // modify the internals of the map.  Put another way, the lambda does not
    // become a member function of the class.  So, for example, if we wanted
    // to add a suffix to every key, then the only way we could do it would
    // be by explicitly updating the k/v pair in the map.
    string suf = " <3";
    // note: we need to capture *kvpairs itself* by reference
    func = [&kvpairs, suf] (string k, string v) { kvpairs[k] = v+suf; };
    kvpairs.apply_to_all(func);
    // use a lambda to print it out
    func = [](string k, string v) { cout << "{" << k << ", " << v << "} "; };
    cout << "Modified kvpairs = ";
    kvpairs.apply_to_all(func);
    cout << endl;
}

Don't forget that you need to use the -std=c++11 flag when you compile this with g++.

No comments:

Post a Comment