📜 ⬆️ ⬇️

Declarative C ++ programming

On Friday, I had a free evening, such when there is no urgent business, and non-urgent to do laziness and want something for the soul. For the soul, I decided to see some report of CppCon 2015 which took place just over a month ago. As a rule, I never have enough time for video reports live, but it all happened that way - a month went by, C ++ - 17 was already on the nose and the conference was supposed to be interesting, but no one had written anything about it, and then evening free. In general, I quickly poked my mouse at the first headline that attracted my attention: Andrei Alexandrescu “Declarative Control Flow" and had a nice evening. Then I decided to share a free retelling with the community.
Let's remember what is usual for C ++ Explicit Flow Control, write a transactionally stable function for copying a file, stable in the sense that it has only two outcomes: either it completes successfully or fails for some reason, but it has no side effects. (beautiful expression - successful failure). The task looks trivial, especially if you use the boost :: filesystem:
void copy_file_tr(const path& from, const path& to) { path tmp=to+".deleteme"; try { copy_file(from, tmp); rename(tmp, to); } catch(...) { ::remove(tmp.c_str()); throw; } } 
Whatever happens during copying, the temporary file will be deleted, which is what we needed. However, if you look at just three lines of meaningful code, all the rest is a check of the success of a function call via try / catch, that is, manual control of execution. The program structure here does not reflect the real logic of the task. Another unpleasant moment is that this code strongly depends on the obviously undescribed properties of the called functions, so the rename () function is assumed to be atomic (transactionally stable), and remove () should not throw exceptions (why here it is used :: remove () instead of boost: : filesystem :: remove ()). Let's make it even worse and write the move_file_tr pair function:
 void move_file_tr(const path& from, const path& to) { copy_file_tr(from, to); try { remove(from); } catch(...) { ::remove(to.c_str()); throw; } } 
We see all the same problems here, in such a tiny piece of code we had to add another try / catch block. Moreover, even here you can already see how badly this code is scaled, each block enters its own scope, the intersection of blocks is impossible, etc. If you are not convinced yet, the standard recommends minimizing the manual use of try / catch, for “verbose and non-trivial uses error-prone.” Let's say directly and honestly that direct control of performance details does not suit us anymore, we want more .
The declarative style instead focuses on the description of the goals, with detailed instructions for achieving them being kept to the necessary minimum, the code is executed in the right way without direct control over the execution of each step. It might sound like a fantasy, but such languages ​​are around us and we use them every day without thinking. Look - SQL, make, regex, they are all declarative in nature. What can we use in C ++ to achieve this effect?
RAII and destructors are declarative because they are called implicitly, as well as the close idiom ScopeGuard. Let's see how the SCOPE_EXIT macro is organized using ScopeGuard, this is actually a rather old trick, suffice it to say that the macro of the same name is present in boost starting from version 1.38. And yet, the repetition is the mother of learning:
 namespace detail { enum class ScopeGuardOnExit {}; template<typename<Fun> ScopeGuard<Fun> operator+ (ScopeGuardOnExit, Fun&& fn) { return ScopeGuard<Fun>(std::forward<Fun>(fn)); } } #define SCOPE_EXIT \ auto ANONIMOUS_VARIABLE(SCOPE_EXIT_STATE) \ = ::detail::ScopeGuardOnExit + (&)[] } 
In fact, this is half the definition of a lambda function, the body must be added when calling.
Here, everything is quite straightforward, an anonymous variable is created containing ScopeGuard, which contains a lambda function defined immediately after the macro call and which function will be called in the destructor of this variable, which sooner or later but when it goes out of scope will be called. (The air ran out of the lungs, otherwise I would add a couple of additional ones)
For completeness, the auxiliary macros look like this:
 #define CONACTENATE_IMPL(s1,s2) s1##s2 #define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2) #define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__) 
With the use of such a construction, the usual C ++ code at once acquires the features unseen at once:
 void fun() { char name[] = "/tmp/deleteme.XXXXXX"; auto fd = mkstemp(name); SCOPE_EXIT { fclose(fd); unlink(name); }; auto buf = malloc(1024*1024); SCOPE_EXIT { free(buf); }; ... } 
So, it is argued that for a full transition to the declarative style, it is enough for us to define two more similar macros - SCOPE_FAIL and SCOPE_SUCCESS; using this triple, you can separate the logically significant code and detailed control instructions. For this we need and it is enough to know whether the destructor is called, normally or as a result of unwinding the stack. And such a function is in C ++ - bool uncaught_exception () , it returns true if it was called from inside a catch block. However, there is one unpleasant nuance - this function is broken in the current version of C ++ and does not always return the correct value. The fact is that it does not distinguish whether the call to the destructor is part of unwinding the stack or whether it is a regular object on the stack created inside the catch block, you can read more about it from the original source . Anyway, in C ++ - 17 this function will be officially declared deprecated and another entered instead - int uncaught_exceptions () (find the two differences yourself), which returns the number of nested handlers from which it was called. We can now create a helper class that shows exactly, call SCOPE_SUCCESS or SCOPE_FAIL:
 class UncaughtExceptionCounter { int getUncaughtExceptionCount() noexcept; int exceptionCount_; public: UncaughtExceptionCounter() : exceptionCount_(std::uncaught_exceptions()) {} bool newUncaughtException() noexcept { return std::uncaught_exceptions() > exceptionCount_; } }; 
It's funny that this class itself also uses RAII to capture the state in the constructor.
Now you can draw a full-fledged template that will be called in case of success or failure:
 template <typename FunctionType, bool executeOnException> class ScopeGuardForNewException { FunctionType function_; UncaughtExceptionCounter ec_; public: explicit ScopeGuardForNewException(const FunctionType& fn) : function_(fn) {} explicit ScopeGuardForNewException(FunctionType&& fn) : function_(std::move(fn)) {} ~ScopeGuardForNewException() noexcept(executeOnException) { if (executeOnException == ec_.isNewUncaughtException()) { function_(); } } }; 
Actually, all the interesting is concentrated in the destructor, it is there that the state of the exception counter is compared with the template parameter and the decision is made to call or not the internal functor. Pay attention as the same template parameter delicately defines the destructor signature: noexcept (executeOnException) , since SCOPE_FAIL should be exception safe, and SCOPE_SUCCESS can completely exclude itself, finally, from harm. In my opinion, it is precisely such minor architectural details that make C ++ the very language I like.
Then everything becomes trivial, like SCOPE_EXIT we define a new macro:
 enum class ScopeGuardOnFail {}; template <typename FunctionType> ScopeGuardForNewException< typename std::decay<FunctionType>::type, true> operator+(detail::ScopeGuardOnFail, FunctionType&& fn) { return ScopeGuardForNewException< typename std::decay<FunctionType>::type, true >(std::forward<FunctionType>(fn)); } #define SCOPE_FAIL \ auto ANONYMOUS_VARIABLE(SCOPE_FAIL_STATE) \ = ::detail::ScopeGuardOnFail() + [&]() noexcept 
And similarly for SCOPE_EXIT
Let's see how the source examples will now look:
 void copy_file_tr(const path& from, const path& to) { bf::path t = to.native() + ".deleteme"; SCOPE_FAIL { ::remove(t.c_str()); }; bf::copy_file(from, t); bf::rename(t, to); } void move_file_tr(const path& from, const path& to) { bf::copy_file_transact(from, to); SCOPE_FAIL { ::remove(to.c_str()); }; bf::remove(from); } 
The code looks more transparent, moreover, each line means something. Here is an example of using SCOPE_SUCCESS, along with a demonstration of why this macro can throw exceptions:
 int string2int(const string& s) { int r; SCOPE_SUCCESS { assert(int2string(r) == s); }; ... return r; } 
Thus, a very small syntactic barrier prevents us from adding another declarative style to C ++.

Conclusion from the first person

All this leads to certain thoughts about what can wait for us in the near future. First of all, it struck me that all the references in the report are far from new. For example, SCOPE_EXIT is present in boost.1.38, that is, for almost ten years, and the article about Alexandrescu himself about ScopeGuard appeared in Dr.Dobbs already in the 2000th year. I would like to remind you that Alexandrescu has a reputation as a visionary and prophet, so he created Loki as a demonstration of the concept of the library which formed the basis of boost :: mpl, and then almost completely entered the new standard and long before that actually set the idioms of metaprogramming. On the other hand, Aleksandrescu himself has recently been mainly engaged in the development of the D language, where all three of the above constructions are scope exit, scope success and scope failure are part of the syntax of the language and have long occupied a strong place in it.
Another interesting point is that the report of Eric Niebler at the same conference is called the Ranges for the Standard Library . I want to remind that ranges are another standard concept of the D language, further development of the concept of iterators. Moreover, the report itself is in fact a translation (from D to C ++) of the remarkable article HSTeoh Component programming with ranges .
Thus, it seems that C ++ began to actively include the concepts of other languages, which however he himself initiated. In any case, the upcoming C ++ - 17 does not seem to be a routine update. Considering the lessons of history, the seventeenth year is not boring, we stock up on popcorn, pineapples and grouse.

Literature

Here, the links already included in the post are simply collected in one place.
  1. Original audio report
  2. Link to CppCon 2015 materials
  3. Slides to the report Alexandrescu
  4. Link to the original article about ScopeGuard 2000
  5. Documentation on boost :: ScopeExit
  6. Herb Sutter's suggestion for changing uncaught_exception ()
  7. The original article on ranges in D , who cares, is a good informal introduction to one of the aspects of this language.

')

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


All Articles