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::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
std::function<int(int, int)> foo_func;
int foo(int a, int b) { ... }
foo_func = foo;
std::function
to the Rescue (2)¶
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::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