Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

How to write decorator functions in modern C++

NotificationsYou must be signed in to change notification settings

TheMaverickProgrammer/C-Python-like-Decorators

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

How to write decorator functions in modern C++14 or higher

Works across MSVC, GNU CC, and Clang compilers

Skip the tutorial and view the final results

tutorial demo

practical demo

compile-time decorator demo

run-time member function demo

reusable member function demo

The goal

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.

Accepting any arbitrary functor in modern C++

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:

goto godbolt

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...

Returning a closure that can accept any number of args

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;    };

Clang and MSVC -auto pack for the win

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!

Nested decorators

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...

goto godbolt

// 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

Works out of the box as-is

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? :)

Practical examples

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;}

Decorating functions at compile-time!

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.

goto godbolt

/////////////////////////////////////////// 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!

Further Applications: Decorating member functions

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.

goto godbolt

// 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

Reusable member-function decorators

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.

goto godbolt

// 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

Writing Python's @classmethod

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&:

goto godbolt

//////////////////////////////////////    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.

After-thoughts

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

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp