您的位置:首页 > 其它

Extreme type erasure via std::function 通过模板(而不是继承)实现接口

2017-04-26 15:19 681 查看
转自: https://a4z.bitbucket.io/blog/2017/01/11/exterm_typererasure-using-std::function.html

本文:

1.介绍了一种通过模板(而不是继承)实现接口的方法,并且可以将不同类型(具有接口指定方法)的对象放入同一个容器。

2.介绍了通过std::function对可调用对象进行类型擦除。

If you have a good title add some content…​ so I did and you can read the result here ;-)

Some time ago, I think it was 2013, Sean Parent gave a talk where he demonstrated how to use type erasure to avoid implementing interfaces in classes.

The talk was called “Inheritance is the base class of evil” and you can find it online. If you have not already seen it you should watch it.


Summarize type erasure

This summary is maybe to short and handles not all aspects, but that’s OK. I do not what to handle all aspects but rather keep it simple.


A classical way

You have different type of classes, like TextWidget, BoxWedget, …​.

You want to have instances of those types in a list

You want to call a method, like for example 
draw
, on each of the objects in the list

The common way to implement this is having a IDrawable interface with a draw method and each class needs to implement this interface. Than you have a bunch of 'drawable' objects and have a list of them, for example in a 
std::vector<IDrawable*>
.


A different way

Sean Parent showed that there is a different way to do this.

After seeing this way my reaction was something of:
What ??? hmmm…​. ??? …​ ahhh …​, of course!
Why did I not think about that myself, why did nobody tell me already years ago…​.

You might know such moments.

A incomplete and to short summary

It is possible to create a drawable_type that can hold different types of classes.

This type has a draw method and calls the draw method of the class it hold.

The drawable_type creates therefor an interface and hides the concrete type behind that.

This is done with the help of templates.

This sounds complicated, but becomes pretty obvious if you see it in code.

So here a short sample implementation that you can copy and use to play with.

#include <iostream>
#include <vector>
#include <memory>

// 2 classes, class1 and class2,
// both have a draw method, but thats all they have in common.
// we want to have class1 and class2  in a list,
// they do not share a type,

struct class1 {
void draw() const {  std::cout << "class1" << std::endl; }
};

struct class2 {
void draw() const { std::cout << "class2" << std::endl; }
};

// so we create a drawable type
class drawable_t {

// drawable knows the concept of drawing objects,
// using a simple abstract class as you know it
struct concept_t {
virtual ~concept_t() = default ;

virtual void draw() const = 0;
};

// A model realize the concept by inheriting it
// What it actually calls is intern and private
// This works as long as:
// 1) the given object is movable
// 2) the given object has a draw method
template< typename T >
struct model_t : concept_t {
// constructing the model
model_t(T x) : erased_type(std::move(x) ) {}

// call draw on whatever it holds
void draw() const  { erased_type.draw(); }

// Whatever it holds member variable
// Some user spcified type what is erased now.
// We only know it has a draw method,
// if not, a a compiler error happens.
T erased_type;
};

// The drawable type has to hold the model.
// No problem, we can do that via the interface.
std::unique_ptr<const concept_t> self_;

public:
// The constructor is a template,
// we don't know what type will be used.
// It has to work with
// every movable type that has a draw method.
template< typename T >
drawable_t(T x) : self_(new model_t<T> (std::move(x)))
{}

// the draw call
void draw() const
{
self_->draw();
}
};

// drawable_t hold model_t<SomeType>
// the requirement of some type is to have a draw method.
// has a pointer to the abstract class concept_t,
// What concept_t pointer points to is a concrete model_t<SomeType>,
// some type is hidden
// we do not know about it when we write drawable_t.
// It is a bit like duck typing.
// If we would use quack instead of draw as the required method
// and call our drawable_t duck.
// We could hide everything that has a quack method inside a duck.

// usage of the code above works like that
int main(int argc, char* argv[])
{
std::vector<drawable_t> objects;

// add whatever movable object that has a draw method
objects.emplace_back(class1());
objects.emplace_back(class2());

// draw all the different typs
for (const auto& object : objects) object.draw();

return 0;
}


You can compile the above code, works also with older compilers like gcc 4.8.x

g++ -std=c++11 sample.cpp


Running the code will, less surprising, look like that

./a.out
class1
class2


So far so good, you did possible already know about that, if not, I hope this summary helped you to understand what this type erasure thing is about.


Extreme type erasure with std::function

Now, to the topic, extreme type erasure with std::function.

I call it like that because I had not other one-liner and it sounds like a good headline.

I recently had something like 'sendable' objects but they did not have anything in common.

No same named function implemented, and some of the objects where even functions them self!

But I needed dynamic assembled lists of all these things that send something and call them in order as they are in the list.

To make it as short as possible, the solution I have chosen looks something like that:

std::vector<std::function<void()>> senderlist ;


And then filling 
senderlist
 it with lambdas that capture what ever they do and need.


And that’s it?

Yes, of course!

This is super short, super flexible and super simple, isn’t it?

Via std::function I can put anything in the call list, no matter if it makes quack, muhh or määä.

Of course the signature of the function object in the vector might vary, also what type of callable objects are in there and what they capture. And in reality it does.

But no need to keep this post not as simple as possible.

I think we all got the point that this is just an other of those "of course you can do it like that" things.

But it took me a moment to realize that this is also type erasure, just more extreme, and therefore the title of this post.


The example

And just to see that what I am talking about also compiles, here a short sample:

#include <iostream>
#include <vector>
#include <functional>

void sendto(int dest, int data) {
std::cout << "sendto" << dest << ": " << data << std::endl ;
}

struct Foo{
void send_1(int data) {
std::cout << "send foo 1: " << data << std::endl;
}

void send_2(int data){
std::cout << "send foo 2: " << data << std::endl;
}
};

struct Bar{
int data ;
void send() { std::cout << "send bar " << data <<std::endl; }
};

int main(int argc, char* argv[])
{
std::vector<std::function<void()>> sender;

int data = 12 ;
sender.emplace_back([data]{sendto(0,data);}) ;

Foo f;
data = 34;
sender.emplace_back([&f, data]() { f.send_1(data) ; }) ;

data = 56;
sender.emplace_back([&f, data]() { f.send_2(data) ; }) ;

Bar b{78};
sender.emplace_back([&b]() { b.send() ; }) ;

for (auto& send : sender)
send();

return 0;
}

// compile with g++ std=c++11 ..
// running will print

// sendto0: 12
// send foo 1: 34
// send foo 2: 56
// send bar 78


That’s it, extreme type erasure via std::function. :-)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐