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();
}
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::bindobjects 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
std::function<int(int, int)> foo_func;
int foo(int a, int b) { ... }
foo_func = foo;
std::function to the Rescue (2)#
std::bind object#struct bar {
int foo(int a, int b) { ... }
};
foo_func = std::bind(&bar::foo, &bar,
std::placeholders::_1, std::placeholders::_2);
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::functionobject 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