Recommendations + syntax for implementing "contextmanager" in C ++

Our Python codebase has metric-related code that looks like this:

class Timer: def __enter__(self, name): self.name = name self.start = time.time() def __exit__(self): elapsed = time.time() - self.start log.info('%s took %f seconds' % (self.name, elapsed)) ... with Timer('foo'): do some work with Timer('bar') as named_timer: do some work named_timer.some_mutative_method() do some more work 

In Python terminology, a timer is a context manager.

Now we want to implement the same thing in C ++ with equally nice syntax. Unfortunately, C ++ does not have a with . So the “obvious” idiom would be (classic RAII)

 class Timer { Timer(std::string name) : name_(std::move(name)) {} ~Timer() { /* ... */ } }; if (true) { Timer t("foo"); do some work } if (true) { Timer named_timer("bar"); do some work named_timer.some_mutative_method(); do some more work } 

But this is a very ugly syntax salt: it is a lot of lines longer than necessary, we had to enter the name t for our "unnamed" timer (and the code breaks silently if we forget this name). it's just ugly.

What are some syntactic idioms that people used for "context managers" in C ++?


I thought of this offensive idea that reduces the number of lines, but does not get rid of the name t :

 // give Timer an implicit always-true conversion to bool if (auto t = Timer("foo")) { do some work } 

Or is it an architectural monster that I do not even believe in the correct use of:

 Timer("foo", [&](auto&) { do some work }); Timer("bar", [&](auto& named_timer) { do some work named_timer.some_mutative_method(); do some more work }); 

where the Timer constructor really calls the given lambda (with the *this argument) and writes all the records in one go.

However, none of these ideas seems to be "best practice." Help me here!


Another way to tell the question might be: If you designed std::lock_guard from scratch, how would you do this to eliminate as many patterns as possible? lock_guard is a great example of a context manager: it is a utility, it is essentially RAII, and you hardly want to name it.

+9
source share
4 answers

You can pretty closely simulate the syntax and semantics of Python. The following test case compiles and has semantics pretty much similar to what Python would have been:

 // https://github.com/KubaO/stackoverflown/tree/master/questions/pythonic-with-33088614 #include <cassert> #include <cstdio> #include <exception> #include <iostream> #include <optional> #include <string> #include <type_traits> [...] int main() { // with Resource("foo"): // print("* Doing work!\n") with<Resource>("foo") >= [&] { std::cout << "1. Doing work\n"; }; // with Resource("foo", True) as r: // r.say("* Doing work too") with<Resource>("bar", true) >= [&](auto &r) { r.say("2. Doing work too"); }; for (bool succeed : {true, false}) { // Shorthand for: // try: // with Resource("bar", succeed) as r: // r.say("Hello") // print("* Doing work\n") // except: // print("* Can't do work\n") with<Resource>("bar", succeed) >= [&](auto &r) { r.say("Hello"); std::cout << "3. Doing work\n"; } >= else_ >= [&] { std::cout << "4. Can't do work\n"; }; } } 

It is given

 class Resource { const std::string str; public: const bool successful; Resource(const Resource &) = delete; Resource(Resource &&) = delete; Resource(const std::string &str, bool succeed = true) : str(str), successful(succeed) {} void say(const std::string &s) { std::cout << "Resource(" << str << ") says: " << s << "\n"; } }; 

The free with function passes all the work to the with_impl class:

 template <typename T, typename... Ts> with_impl<T> with(Ts &&... args) { return with_impl<T>(std::forward<Ts>(args)...); } 

How do we get there? First, we need the context_manager class: a feature class that implements the enter and exit methods - the Python equivalents __enter__ and __exit__ . Once an attribute of type is_detected been added in C ++, this class can also easily forward compatible enter and exit methods of type T , thus imitating Python semantics even better. In its current form, the context manager is quite simple:

 template <typename T> class context_manager_base { protected: std::optional<T> context; public: T &get() { return context.value(); } template <typename... Ts> std::enable_if_t<std::is_constructible_v<T, Ts...>, bool> enter(Ts &&... args) { context.emplace(std::forward<Ts>(args)...); return true; } bool exit(std::exception_ptr) { context.reset(); return true; } }; template <typename T> class context_manager : public context_manager_base<T> {}; 

Let's see how this class will be specialized for packing Resource or std::FILE * objects.

 template <> class context_manager<Resource> : public context_manager_base<Resource> { public: template <typename... Ts> bool enter(Ts &&... args) { context.emplace(std::forward<Ts>(args)...); return context.value().successful; } }; template <> class context_manager<std::FILE *> { std::FILE *file; public: std::FILE *get() { return file; } bool enter(const char *filename, const char *mode) { file = std::fopen(filename, mode); return file; } bool leave(std::exception_ptr) { return !file || (fclose(file) == 0); } ~context_manager() { leave({}); } }; 

The implementation of basic functionality is of type with_impl . Notice how the exception handling in the set (first lambda) and the exit function mimic Python behavior.

 static class else_t *else_; class pass_exceptions_t {}; template <typename T> class with_impl { context_manager<T> mgr; bool ok; enum class Stage { WITH, ELSE, DONE } stage = Stage::WITH; std::exception_ptr exception = {}; public: with_impl(const with_impl &) = delete; with_impl(with_impl &&) = delete; template <typename... Ts> explicit with_impl(Ts &&... args) { try { ok = mgr.enter(std::forward<Ts>(args)...); } catch (...) { ok = false; } } template <typename... Ts> explicit with_impl(pass_exceptions_t, Ts &&... args) { ok = mgr.enter(std::forward<Ts>(args)...); } ~with_impl() { if (!mgr.exit(exception) && exception) std::rethrow_exception(exception); } with_impl &operator>=(else_t *) { assert(stage == Stage::ELSE); return *this; } template <typename Fn> std::enable_if_t<std::is_invocable_r_v<void, Fn, decltype(mgr.get())>, with_impl &> operator>=(Fn &&fn) { assert(stage == Stage::WITH); if (ok) try { std::forward<Fn>(fn)(mgr.get()); } catch (...) { exception = std::current_exception(); } stage = Stage::ELSE; return *this; } template <typename Fn> std::enable_if_t<std::is_invocable_r_v<bool, Fn, decltype(mgr.get())>, with_impl &> operator>=(Fn &&fn) { assert(stage == Stage::WITH); if (ok) try { ok = std::forward<Fn>(fn)(mgr.get()); } catch (...) { exception = std::current_exception(); } stage = Stage::ELSE; return *this; } template <typename Fn> std::enable_if_t<std::is_invocable_r_v<void, Fn>, with_impl &> operator>=(Fn &&fn) { assert(stage != Stage::DONE); if (stage == Stage::WITH) { if (ok) try { std::forward<Fn>(fn)(); } catch (...) { exception = std::current_exception(); } stage = Stage::ELSE; } else { assert(stage == Stage::ELSE); if (!ok) std::forward<Fn>(fn)(); if (!mgr.exit(exception) && exception) std::rethrow_exception(exception); stage = Stage::DONE; } return *this; } template <typename Fn> std::enable_if_t<std::is_invocable_r_v<bool, Fn>, with_impl &> operator>=(Fn &&fn) { assert(stage != Stage::DONE); if (stage == Stage::WITH) { if (ok) try { ok = std::forward<Fn>(fn)(); } catch (...) { exception = std::current_exception(); } stage = Stage::ELSE; } else { assert(stage == Stage::ELSE); if (!ok) std::forward<Fn>(fn)(); if (!mgr.exit(exception) && exception) std::rethrow_exception(exception); stage = Stage::DONE; } return *this; } }; 
+4
source

Edit: after reading the comment Give more carefully and thinking a little more, I realized that this is a bad choice for C ++ RAII. What for? Since you enter the destructor, this means that you are doing io, and io can throw. C ++ destructors should not throw exceptions. With python, writing to throw __exit__ also not necessarily surprising, it can lead to the fact that you leave the first exception on the floor. But in python, you finally know whether the code in the context manager threw an exception or not. If this throws an exception, you can simply omit the registration in __exit__ and go through the exception. I leave my initial answer below if you have a context manager who does not risk throwing it out.

The C ++ version is 2 lines longer than the python version, one for each curly brace. If C ++ is only two lines longer than python, this is good. Context managers are designed for this specific purpose, RAII is more general and provides a strict superset of functionality. If you want to get acquainted with the best practice, you have already found it: you have an anonymous scope and create an object at the beginning. This is idiomatic. You may find it ugly coming from python, but in C ++ the world is just fine. Similarly, someone in C ++ will find context managers ugly in certain situations. FWIW I use both languages ​​professionally and it doesn’t bother me at all.

However, I will provide a cleaner approach for anonymous context managers. Your approach to building a timer with a lambda and immediately destroying it is rather strange, so you are right to be suspicious. The best approach:

 template <class F> void with_timer(const std::string & name, F && f) { Timer timer(name); f(); } 

Using:

 with_timer("hello", [&] { do something; }); 

This is equivalent to an anonymous context manager in the sense that none of the Timer methods can be called except for construction and destruction. In addition, it uses the “normal” class, so you can use the class when you need a named context manager, and this function otherwise. You could obviously write with_lock_guard in a very similar way. There it is even better, since lock_guard does not have any member functions that you skip.

All that said, would I use with_lock_guard or approve code written by a teammate added to such a utility? No. One or two additional lines of code simply do not matter; this function does not add sufficient utility to justify its existence. YMMV.

+2
source

You do not need if( true ) , C ++ has "anonymous areas" that can be used to limit the lifetime of the area in the same way as Python with or C # s using (well, C # also has anonymous areas )

Same:

 doSomething(); { Time timer("foo"); doSomethingElse(); } doMoreStuff(); 

Just use the bare braces.

However, I do not agree with your idea of ​​using RAII semantics for tool code like this, since the timer destructor is non-trivial and has side effects on the design. This may be ugly and repetitive, but I explicitly call methods with the names startTimer , stopTimer and printTimer to make the program more “correct” and self-documenting. Side effects are bad, m'key?

+1
source

I recently started a C ++ project that mimics the Python context manager when I ported the Python codebase to C ++ located at https://github.com/batconjurer/contextual .

For this stream, you need to define a resource manager obtained from an interface named IResource . Below it is called Timer . It is in this class that the enter and exit functions are implemented. A context is simply a block of code that requires resources, so it is passed through an anonymous function.

The resource manager expects you to implement the IData structure, which is used to store received resources. In fact, it contains only a pointer to an IData instance.

For your use case, below is an example implementation that compiles with C ++ 17.

 #include "include/contextual.h" #include <ctime> #include <chrono> #include <thread> using namespace Contextual; namespace Contextual { struct IData { std::string name; std::time_t start_time = std::time(NULL); void reset_time() { std::cout << "Time before restart: " << start_time << "\n"; std::time(&start_time); std::cout << "Time after restart: " << start_time << "\n"; }; }; class Timer : public IResource<IData> { private: IData _data; void enter() override { std::time(&resources->start_time); } void exit(std::optional<std::exception> e) override { double elapsed_time = std::time(NULL) - resources->start_time; std::cout << resources->name << " took " << elapsed_time << " seconds.\n"; if (e) { throw e.value(); } } public: Timer(std::string &&name) : IResource<IData>(_data), _data(IData{name}){}; }; }; int main(){ With { Timer(std::string("Foo")) + Context{ [&](IData* time_data) { std::chrono::milliseconds sleeptime(5000); std::this_thread::sleep_for(sleeptime); // In place of "Do some work" time_data->reset_time(); // In place of "some_mutative_function()" std::this_thread::sleep_for(sleeptime); // In place of "Do some work" } } }; } 

There are some troubles that I'm still working on (for example, the fact that the IData structure should have been stored as a Timer instance variable, since the IResource only stores a pointer to it). And, of course, C ++ exceptions are not the best part.

0
source

Source: https://habr.com/ru/post/1233551/


All Articles