- Notifications
You must be signed in to change notification settings - Fork14
TheMaverickProgrammer/C-Python-like-Decorators
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
How to write decorator functions in modern C++14 or higher
Works across MSVC, GNU CC, and Clang compilers
Python has a nice feature that allows function definitions to be wrapped by other existing functions. "Wrapping" consists of taking a function in as an argument and returning a new aggregated function composed of the input function and the wrapper function. The wrapper functions themselves are called 'decorator' functions because they decorate, or otherwise extend, the original input function's behavior.
The syntax for this in python begin with@
immediately followed by the decorator function name. On the next line, a brand new function definition begins. The result is that the new function will be automatically decorated by the function we declared with the@
symbol. A python decorator would look like this:
defstars(func):definner(*args,**kwargs):print("**********")func(*args,**kwargs)print("**********")returninner# decorator syntax@starsdefhello_world():print("hello world!")# The following prints:## **********# hello world!# **********hello_world()
The decorating functionstars(...)
will take in any kind of input and pass it along to its innerfunc
object, whatever that may be. In this case,hello_world()
does not take in any arguments sofunc()
will simply be called.
The@stars
syntax is sugar forhello_world = stars(hello_world)
.
This is a really nice feature for python that, as a C++ enthusiast, I would like to use in my own projects. In this tutorial I'm going to make a close equivalent without using magic macros or meta-object compilation tools.
Python decorator functions take in afunction as its argument. I toyed with a variety of concepts and discovered quickly that lambdas are not as versatile as I had hoped they would be. Consider the following code:
template<typename R,typename... Args>autostars(R(*in)(Args...)) {return [in](Args&&... args) { std::cout <<"*******" << std::endl;in(); std::cout <<"*******" << std::endl; };}voidhello() {cout <<"hello, world!" << endl;}template<typename R,typename... Args>autosmart_divide(R(*in)(Args...)) {return [in](float a,float b) { std::cout <<"I am going to divide a and b" << std::endl;if(b ==0) { std::cout <<"Whoops! cannot divide" << std::endl;return0.0f; }returnin(a, b); };}floatdivide(float a,float b) {return a/b;}
This tries to achieve the following python code:
defsmart_divide(func):definner(a,b):print("I am going to divide",a,"and",b)ifb==0:print("Whoops! cannot divide")returnreturnfunc(a,b)returninner@smart_dividedefdivide(a,b):returna/b
It works! Great! But try uncommenting line 66. It does not compile. If you looks closely at the compiler output it has trouble deducing the function pointer types from the lambda object returned by the inner-most decorator. This is becauselambdas with capture cannot be converted to function pointers
If we were to introduce a struct to hold our arbitrary functors, we'd fall into the same problem. By limiting ourselves to a specific expected function-pointer syntax, we lose the ability to accept just about any type we want. The solution is to use a singletemplate<typename F>
before our decorators to let the compiler know the decorator can take in just about anything we throw at it.
Now we can nest multiple decorator functions together! ... not so fast...
Now we've lost the type information fromArgs...
in our function signature. Luckily there is something we can do about this in C++14 and onward...
We need to build a function, in our function, that can also accept an arbitrary set of inputs and pass those along to our captured input function. To reiterate, ourreturned function needs to be able toforward all the arguments to the function we are trying to decorate.
Python gets around this problem by using special arguments*args
and**kwargs
. I won't go into detail what these two differerent notations mean, but for our problem task they are equivalent to C++ variadic arguments. They can be written like so:
template<typename... Args>voidfoo(Args&&... args) {bar(std::forward<decltype(args)>(args)...);}
Anything passed into the function are forwarded as arguments for the inner functionbar
. If the types match, the compiler will accept the input. This is what we want, but remember we're returning a function inside another function. Prior to C++14 this might have been impossible to achieve nicely. Thankfully C++14 introducedtemplate lambdas
return [func]<typename... Args>(Args&&... args) { std::cout <<"*******" << std::endl;func(std::forward<decltype(args)>(args)...);// forward all arguments std::cout <<"\n*******" << std::endl; };
Try toggling the compiler options in godbolt from GNU C Compiler 9.1 to latest Clang or MSVC. No matter what standard you specify, it won't compile. We were so close! Let's inspect further. Some google searches and forum scrolling later, it seems the trusty GCC might be ahead of the curve with generic lambdas in C++14. To quote one forum user:
C++20 will come with templated and conceptualized lambdas. The feature has already been integrated into the standard draft.
I was stuck on this for quite some time until I discovered this neat trick by trial and error:
template<typename F>autooutput(const F& func) {return [func](auto&&... args) { std::cout <<func(std::forward<decltype(args)>(args)...); };}
By specifying the arguments for the closure as anauto
pack, we can avoid template type parameters all together - much more readable!
Great, we can begin putting it all together! We have:
- Function that returns function (by lambda closures) (CHECK)
- "Decorator function" that can accept any input function using a template typename (CHECK)
- Inner function can also accept arbitrary args passed from outer function (using auto packs) (CHECK)
Now we can check to see if we can further nest the decorators...
// line 57 -- four decorator functions!auto d = stars(output(smart_divide(divide)));d(12.0f,3.0f);
output is
*******I am going to divide a=12and b=34*******
First thestars
decorator is called printing**********
to our topmost row.Then theoutput
function prints the result of the next function tocout
so we can see it. The result of the next function is covered by the next two nested functions.
Thesmart_divide
function checks the input passed in from the top of the chain12.0f, 3.0f
if we are dividing by zero or not before forwarding args to the next functiondivide
which calculates the result.divide
returns the result andsmart_divide
returns that result tooutput
.
Finallystars
scope is about to end and prints the last*********
row
Check out line 51 usingprintf
auto p = stars(printf);p("C++ is %s!","epic");
output is
*******C++ is epic!*******
I think I found my new favorite C++ concept for outputting log files, don't you feel the same way? :)
There's a lot of debate about C++'s exception handling, lack thereof, and controversial best practices. We can solve a lot of headache by providing decorator functions to let throwable functions fail without fear.
Consider this example:goto godbolt
We can let the function silently fail and we can choose to supply another decorator function to pipe the output to a log file. Alternatively we could also check the return value of the exception (using better value types of course this is just an example) to determine whether to shutdown the application or not.
auto read_safe = exception_fail_safe(file_read);// assume read_safe() returns some optional_type<> structif(!read_safe("missing_file.txt", buff, &sz).OK) {// Whoops! We needed this file. Quit immediately! app.abort();return;}
After this tutorial was released a user by the online namerobin-m pointed out that the functionscould be decorated at compile-time as opposed to runtime (as I previously acknowledged this seemed to be the only way in C++ without macro magic). Robin-m suggests usingconstexpr
in the function declaration.
/////////////////////////////////////////// final decorated functions ///////////////////////////////////////////constexprauto hello = stars(hello_impl);constexprauto divide = stars(output(smart_divide(divide_impl)));constexprauto print = stars(printf);intmain() {// ... }
This allows us to separate the regular function implementation we may wish to decorate from the final decorated function we'll use in our programs. This means any modification to these 'final' functions will happen globally across our code base and not limited to a single routine's scope. This increases reusability, modularity, and readability - no sense repeating yourself twice!
We can decorate member functions in C++. To be clear, we cannot change the existing member function itself, but we can bind a reference to the member function and call it.
Let's take an example that uses everything we learned so far. We want to produce a grocery checkout program to tell us the cost of each bag of apples we picked. We want to throw exceptions when invalid arguments are supplied but we want it to do so safely, log a nice timestamp somewhere, and display the price if valid.
// exception decorator for optional return typestemplate<typename F>autoexception_fail_safe(const F& func) {return [func](auto&&... args) -> optional_type<decltype(func(std::forward<decltype(args)>(args)...))> {using R = optional_type<decltype(func(std::forward<decltype(args)>(args)...))>;try {returnR(func(std::forward<decltype(args)>(args)...)); }catch(std::iostream::failure& e) {returnR(false, e.what()); }catch(std::exception& e) {returnR(false, e.what()); }catch(...) {// This ... catch clause will capture any exception thrownreturnR(false,std::string("Exception caught: default exception")); } };}
This decorator returns anoptional_type
which for our purposes is very crude but allows us to check if the return value of the function was OK or if an exception was thrown. If it was, we want to see what it is. We declare the lambda to share the same return value as the closure with-> optional_type<decltype(func(std::forward<decltype(args)>(args)...))>
. We use the same try-catch as before but supply different constructors for ouroptional_type
.
We now want to use this decorator on ourdouble apple::calculate_cost(int, double)
member function. We cannot change what exists, but we can turn it into a functor usingstd::bind
.
applesgroceries(1.09);auto get_cost = exception_fail_safe(std::bind(&apples::calculate_cost, &groceries, _1, _2));
We create a vector of 4 results. 2 of them will throw errors while the other 2 will run just fine. Let's see our results.
[1] There was an error: apples must weigh more than0 ounces[2] Bag cost $2.398[3] Bag cost $7.085[4] There was an error: must have1or more apples
The last example was not reusable. It was bound to exactly one object. Let's refactor that and decorate our output a little more.
template<typename F>autovisit_apples(const F& func) {return [func](apples& a,auto&&... args) {return (a.*func)(std::forward<decltype(args)>(args)...); };}
All we need is a function at the very deepest part of our nest that takes in the object by reference and its member function.
We could wrap it like so
applesgroceries(2.45);auto get_cost = exception_fail_safe(visit_apples(&apples::calculate_cost));get_cost(groceries,10,3);// lots of big apples!
Crude but proves a point. We've basically reinvented a visitor pattern. Our decorator functions visit an object and invoke the member function on our behalf since we cannot modify the class definition. The rest is the same as it was before: functions nested in functions like little russian dolls.
We can completely take advantage of this functional-like syntax and have all our output decorators return the result value as well.
// Different prices for different applesapplesgroceries1(1.09), groceries2(3.0), groceries3(4.0);auto get_cost = log_time(output(exception_fail_safe(visit_apples(&apples::calculate_cost))));auto vec = {get_cost(groceries2,2,1.1),get_cost(groceries3,5,1.3),get_cost(groceries1,4,0) };
Which outputs
Bag cost $6.6> Logged at Mon Aug502:17:102019Bag cost $26> Logged at Mon Aug502:17:102019There was an error: apples must weigh more than0 ounces> Logged at Mon Aug502:17:102019
In python, we have a similar decorator to properly decorate member functions:@classmethod
. This decorator specifically tells the interpreter to passself
into the decorator chain, if used, so that the member function can be called correctly- specifically in the event of inherited member functions.Further reading on stackoverflow
We needed to pass the instance of the object into the decorator chain and with a quick re-write we can make this class visitor decorate function universal.
Simply swap outapples&
forauto&
:
////////////////////////////////////// visitor function //////////////////////////////////////template<typename F>constexprautoclassmethod(F func) {return [func](auto& a,auto&&... args) {return (a.*func)(args...); };}
Now we can visit any class type member function.
Unlike python, C++ doesn't let us redefine functions on the fly, but we could get closer to python syntax if we had some kind of intermediary functor type that we could reassign e.g.
decorated_functor d = smart_divide(divide);// reassignmentd = stars(output(d));
Unlike python, assignment of arbitrary types in C++ is almost next to impossible without type erasure.We solved this by using templates but every new nested function returns a new type that the compiler sees.And as we discovered at the beginning, lambdas with capture do not dissolve into C++ pointers as we might expect either.
With all the complexities at hand, this task non-trivial.
update!I tackled this designhere making it possible for classes to have re-assignable member function types.
This challenge took about 2 days plugged in and was a lot of fun. I learned a lot on the way and discovered something pretty useful. Thanks for reading!
About
How to write decorator functions in modern C++
Resources
Uh oh!
There was an error while loading.Please reload this page.