std::bind

Why? What’s The Problem?

Clumsy code like below …

  • One-time functions, only used to do one stupid little thing (e.g. define double distance_origin(Point p) in terms of double distance(Point p, Point q))

  • Function definition in places which are far off their call sites ⟶ readability suffers!

Sample Program: All Done Manually

#include <cmath>
#include <iostream>


struct Point
{
    Point(double x, double y)
      : x(x), y(y) {}
    double x, y;
};

double distance(Point p, Point q)
{
    return std::sqrt(
        std::pow(std::abs(p.x-q.x), 2) +
        std::pow(std::abs(p.y-q.y), 2)
    );
}

double distance_origin(Point p)
{
    return distance(p, {0,0});
}

int main()
{
    Point points[]{{1,2}, {3,4}, {5,6}};
    double origin_distances[sizeof(points)/sizeof(Point)];

    // compute origin-distances of points
    int i=0;     
    for (auto p: points)
        origin_distances[i++] = distance_origin(p);

    for (double dist: origin_distances)
        std::cout << dist << std::endl;
    
    return 0;
}

Sideway: std::transform

  • Problem: manual loops to transform an array (or any container) into something else

  • Loop body contains transformation code

  • ⟶ cries for duplicate code elimination (here: using std::transform

  • ⟶ similar pattern across <algorithm> (see here)

    • Sorting criteria: std::sort, std::map, …

    • Predicates: std::find_if, std::equal, …

    • Arbitrary adaptations where helper functions are needed

Sample Program: Using std::transform

Still using the one-time function double distance_origin(Point p) though …

#include <cmath>
#include <iostream>
#include <algorithm>


struct Point
{
    Point(double x, double y)
      : x(x), y(y) {}
    double x, y;
};

double distance(Point p, Point q)
{
    return std::sqrt(
        std::pow(std::abs(p.x-q.x), 2) +
        std::pow(std::abs(p.y-q.y), 2)
    );
}

double distance_origin(Point p)
{
    return distance(p, {0,0});
}

int main()
{
    Point points[]{{1,2}, {3,4}, {5,6}};
    double origin_distances[sizeof(points)/sizeof(Point)];

    // compute origin-distances of points
    std::transform(points, points+sizeof(points)/sizeof(Point), origin_distances, distance_origin);

    for (double dist: origin_distances)
        std::cout << dist << std::endl;
    
    return 0;
}

std::bind: Null Adaptation, Pointlessly

#include <gtest/gtest.h>
#include <functional>

static int void_function()
{
    return 42;
}

TEST(bind_suite, plain_void_function)
{
    auto func = std::bind(void_function);
    ASSERT_EQ(func(), 42);
}

std::bind: Adapting One Parameter To No Parameter

#include <gtest/gtest.h>
#include <functional>

static int one_parameter_function(int i)
{
    return i*2;
}

TEST(bind_suite, plain_one_parameter_function)
{
    auto func = std::bind(one_parameter_function, 1);
    ASSERT_EQ(func(), 2);
}

std::bind: Hardcoding Parameters

#include <gtest/gtest.h>
#include <functional>

static int minus(int i, int j)
{
    return i-j;
}

TEST(bind_suite, minus_hardcoded)
{
    auto _1_minus_2 = std::bind(minus, 1, 2);
    ASSERT_EQ(_1_minus_2(), -1);
}

std::bind: Swapping Parameters ⟶ std::placeholders

#include <gtest/gtest.h>
#include <functional>

static int minus(int i, int j)
{
    return i-j;
}

TEST(bind_suite, minus_swap_parameters)
{
    auto second_minus_first = std::bind(minus, std::placeholders::_2, std::placeholders::_1);
    ASSERT_EQ(second_minus_first(1, 2), 1);
}

std::bind: Hardcoding Only First Parameter ⟶ std::placeholders

#include <gtest/gtest.h>
#include <functional>

static int minus(int i, int j)
{
    return i-j;
}

TEST(bind_suite, minus_hardcode_first_parameter)
{
    auto _42_minus_param = std::bind(minus, 42, std::placeholders::_1);
    ASSERT_EQ(_42_minus_param(1), 41);
}

std::bind: Functor (Is-A Callable)

#include <functional>
#include <gtest/gtest.h>

class Functor
{
public:
    Functor(int i) : _i{i} {}
    int operator()(int addend)
    {
        return _i + addend;
    }

private:
    int _i;
};


TEST(bind_suite, functor)
{
    Functor func{42};
    auto funcfunc = std::bind(func, 1);
    ASSERT_EQ(funcfunc(), 43);
}

std::bind: Lambda (Is-A Callable)

#include <functional>
#include <gtest/gtest.h>

TEST(bind_suite, lambda)
{
    auto func = [val=42](int i){ return val+i; };
    auto funcfunc = std::bind(func, 1);
    ASSERT_EQ(funcfunc(), 43);
}

Sample Program: Using std::transform With std::bind

See how we can eliminate the one-time function double distance_origin(Point p)

#include <cmath>
#include <iostream>
#include <algorithm>
#include <functional>


struct Point
{
    Point(double x, double y)
      : x(x), y(y) {}
    double x, y;
};

double distance(Point p, Point q)
{
    return std::sqrt(
        std::pow(std::abs(p.x-q.x), 2) +
        std::pow(std::abs(p.y-q.y), 2)
    );
}

int main()
{
    Point points[]{{1,2}, {3,4}, {5,6}};
    double origin_distances[sizeof(points)/sizeof(Point)];

    // compute origin-distances of points
    std::transform(points, points+sizeof(points)/sizeof(Point), origin_distances, 
                   std::bind(distance, std::placeholders::_1, Point{0,0}));

    for (double dist: origin_distances)
        std::cout << dist << std::endl;
    
    return 0;
}

Summary

  • Readability: what remains unreadable is only the language itself

  • Have to get used to std::bind

What about types?

  • Goal is to have no runtime overhead

  • Late binding (polymorphism) ruled out

  • ⟶ No common base class

  • Only the call signatures (parameter and return types) are the same

  • If you want to define interfaces (i.e. share a type), use std::function

What does this mean?

  • Perfect for <algorithm> which is also designed for speed

  • Have to be careful when code size is important

  • Client code has to be instantiated with the type

  • Tradeoff: speed, code size, elegance, design, taste …

And Lambdas?

  • Lambdas are usually a better alternative

  • ⟶ more readable (?)