std::function

Classic Polymorphism

Back to classic Object Oriented Design …

  • Interfaces define what methods have to be available on an object

  • Implementations provide those methods

  • Clients use interfaces

#include <iostream>


class Interface
{
public:
    virtual ~Interface() {}
    virtual void do_this() = 0;
    virtual void do_that() = 0;
};

class OneImplementation : public Interface
{
public:
    virtual void do_this()
    {
        std::cout << "OneImplementation doing this" << std::endl;
    }
    virtual void do_that()
    {
        std::cout << "OneImplementation doing that" << std::endl;
    }
};

class AnotherImplementation : public Interface
{
public:
    virtual void do_this()
    {
        std::cout << "AnotherImplementation doing this" << std::endl;
    }
    virtual void do_that()
    {
        std::cout << "AnotherImplementation doing that" << std::endl;
    }
};

class Client
{
public:
    Client(Interface *iface) : interface(iface) {}

    void do_much_work()
    {
        interface->do_this();
        interface->do_that();
    }

private:
    Interface *interface;
};

int main()
{
    OneImplementation one;
    AnotherImplementation another;
    Client c_using_one(&one);
    Client c_using_another(&another);

    c_using_one.do_much_work();
    c_using_another.do_much_work();
}
../../../../../../_images/function-classic-polymorphism.png

Classic Polymorphism: Upsides

Polymorphism is well understood:

  • Late binding: client does not know the exact type that is being used

  • Interfaces describe relationships in almost human language - if done right

  • Software Architecture - if done right - is almost self-explanatory

  • Design Patterns are described (and mostly implemented as well) in such a way

  • Also available in other languages

    • For example Java explicitly distinguishes between interface and implementation

Classic Polymorphism: Technical Downsides

There are purely technical downsides (in C++ at least)

  • Runtime overhead

    • Not knowing the exact type implies indirect call (function pointer/trampoline)

  • Code size

    • If one writes virtual, a whole bunch of code is generated (Runtime Type Information - RTTI)

    • Type is not POD (plain old data) anymore

Classic Polymorphism: More Downsides

Metaphysical downsides are harder to come by: readability again

  • Provided that logging has no architectural relevance …

  • I have two functions which are similar in purpose, but otherwise unrelated. How can I arrange for client code to use these interchangeably?

    • Why can’t I just use them?

    • I don’t want to instantiate client code from a template!

    • Do I really want to craft an interface for client code to use?

  • I have a class that has similar purpose as the functions

    • Client code wants to just call it

  • I want to adapt all these!

  • Sound like the solution is std::bind

  • ⟶ Wrong: std::bind objects don’t share a type

#include <iostream>
#include <string>


class Logger
{
public:
    virtual ~Logger() {}
    virtual void log(uint64_t timestamp, std::string message) = 0;
};

class OStreamLogger : public Logger
{
public:
    OStreamLogger(std::ostream& s) : s(s) {}
    virtual void log(uint64_t timestamp, std::string message)
    {
        s << "(OStreamLogger at work) " << timestamp << ':' << message << std::endl;
    }
private:
    std::ostream& s;
};

class DatabaseLogger : public Logger
{
public:
    virtual void log(uint64_t timestamp, std::string message)
    {
        std::cerr << "(DatabaseLogger logging to big fat DB) " << timestamp << ':' << message << std::endl;
    }
};

typedef void(*logfunc_t)(uint64_t timestamp, std::string message);

class FuncPtrLogger : public Logger
{
public:
    FuncPtrLogger(logfunc_t f) : f(f) {}
    virtual void log(uint64_t timestamp, std::string message)
    {
        f(timestamp, message);
    }
private:
    logfunc_t f;
};

class SomeBusinessClassWithNeedForLogging
{
public:
    SomeBusinessClassWithNeedForLogging(Logger* logger) : logger(logger) {}

    void do_much_work()
    {
        logger->log(42, "SomeBusinessClassWithNeedForLogging about to do much work");
        std::cerr << "SomeBusinessClassWithNeedForLogging doing much work" << std::endl;
        logger->log(666, "SomeBusinessClassWithNeedForLogging successfully did much work");
    }

private:
    Logger* logger;
};

void do_stupid_logging(uint64_t timestamp, std::string message)
{
    std::cerr << "do_stupid_logging at work: " << timestamp << ':' << message << std::endl;
}

int main()
{
    OStreamLogger ostream_logger(std::cerr);
    DatabaseLogger database_logger;
    FuncPtrLogger funcptr_logger(&do_stupid_logging);

    SomeBusinessClassWithNeedForLogging busy_logging_to_ostream(&ostream_logger);
    SomeBusinessClassWithNeedForLogging busy_logging_to_database(&database_logger);
    SomeBusinessClassWithNeedForLogging busy_logging_to_funcptr(&funcptr_logger);

    busy_logging_to_ostream.do_much_work();
    busy_logging_to_database.do_much_work();
    busy_logging_to_funcptr.do_much_work();

    return 0;
}

std::function to the Rescue (1)

  • One type to rule them all!

  • Any callable with same signature

Function object
std::function<int(int, int)> foo_func;
Trivial: plain function
int foo(int a, int b) { ... }
foo_func = foo;

std::function to the Rescue (2)

Any std::bind object
struct bar {
    int foo(int a, int b) { ... }
};
foo_func = std::bind(&bar::foo, &bar,
       std::placeholders::_1, std::placeholders::_2);
Lambda
foo_func = [](int a, int b) -> int { ... };

std::function: Last Words

Upsides

  • Lightweight Polymorphism: no code explosion

  • Unlike heavyweight polymorphism, no dynamic allocation appropriate

    • Although a std::function object can hold polymorphic callables, it is always the same size

Downsides

  • Runtime overhead due to indirect call

    • Processor support makes them just as fast as direct function calls

    • But: no inlining possible

  • Readability again …

    • This is not OO!

    • Architectural intentions not at all obvious through quick inline adaptations