Declarative C ++ Programming
On Friday there was a free evening, such when there is no urgent matter, and non-urgent do laziness and I want something for the soul. For the soul, I decided to see some report CppCon 2015 which took place a little more than a month ago. As a rule, I never have enough time for live video reports, but it all happened like this - a month passed, C ++ - 17 was already on the nose and the conference should have been interesting, but no one had written anything about it, and then it was evening free. In general, I quickly poked my mouse into the first headline that attracted attention: Andrei Alexandrescu “Declarative Control Flow" and had a nice evening. And then I decided to share a free retelling with the habrasociety.
Let's recall what is common with 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 succeeds, or for some reason it fails, but it does not have side effects, (beautiful expression - successful failure). The task looks trivial, especially if you use boost :: filesystem:
Instead, the declarative style focuses on the description of goals, while detailed instructions on how to achieve it are minimized, code execution in the right way occurs 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 hesitation. 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 in nature because they are invoked implicitly, as well as the close idiom ScopeGuard. Let's see how the SCOPE_EXIT macro is arranged using ScopeGuard, this is actually quite an old trick, suffice it to say that the macro of the same name has been present in boost since version 1.38. And yet, the repetition of mother teaching:
Everything is fairly 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 will be called sooner or later, but when leaving the scope. (The air ran out in my lungs, otherwise I would add a couple of accessory clauses)
For completeness, here are the auxiliary macros:
Now you can draw a full-fledged template that will be called in case of success or failure:
Then everything becomes trivial, like SCOPE_EXIT we define a new macro:
Let's see how the original examples will now look:
Another interesting point is Eric Niebler's talk at the same conference called Ranges for the Standard Library . I want to remind you that ranges is another standard concept of the D language, a further development of the concept of iterators. Moreover, the report itself is actually a translation (from D to C ++) of the wonderful article HSTeoh Component programming with ranges .
Thus, it seems that C ++ has begun 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. Given the lessons of history, the seventeenth year is not boring, stocking up with popcorn, pineapple and hazel grouse.
Let's recall what is common with 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 succeeds, or for some reason it fails, but it does not have side effects, (beautiful expression - successful failure). The task looks trivial, especially if you use 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 the copy, the temporary file will be deleted, which is what we needed. However, if you look closely at only three lines of meaningful code, the rest is to verify the success of function calls through try / catch, that is, manual control of execution. The structure of the program here does not reflect the real logic of the problem. Another unpleasant moment is that this code depends heavily on the properties of the functions being called that are not described here, so the rename () function is assumed to be atomic (transactionally stable), and remove () should not throw exceptions (which is why :: remove () is used instead of boost: : filesystem :: remove ()). Let's make it worse and write a pair function move_file_tr: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 notice how poorly such a code scales, each block enters its own scope, block intersection is impossible, etc. If all of this has not convinced you 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 management of execution details no longer suits us, we want more .Instead, the declarative style focuses on the description of goals, while detailed instructions on how to achieve it are minimized, code execution in the right way occurs 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 hesitation. 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 in nature because they are invoked implicitly, as well as the close idiom ScopeGuard. Let's see how the SCOPE_EXIT macro is arranged using ScopeGuard, this is actually quite an old trick, suffice it to say that the macro of the same name has been present in boost since version 1.38. And yet, the repetition of mother teaching:
namespace detail {
enum class ScopeGuardOnExit {};
template ScopeGuard operator+
(ScopeGuardOnExit, Fun&& fn) {
return ScopeGuard(std::forward(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 called. Everything is fairly 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 will be called sooner or later, but when leaving the scope. (The air ran out in my lungs, otherwise I would add a couple of accessory clauses)
For completeness, here are the auxiliary macros:
#define CONACTENATE_IMPL(s1,s2) s1##s2
#define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2)
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__)
Using this design, familiar C ++ code takes on unprecedented features 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 such macros - SCOPE_FAIL and SCOPE_SUCCESS, using this triple you can separate the logically meaningful code and detailed control instructions. To do this, it is necessary and sufficient for us 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 the 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 the stack unwinding or is it a regular object on the stack created inside the catch block, you can read more about this fromprimary source . Be that as it may, in C ++ - 17 this function will be officially declared deprecated and another one will be introduced instead of it - 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 whether to 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 state in the constructor. Now you can draw a full-fledged template that will be called in case of success or failure:
template
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, everything 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 whether or not to call the internal functor. Please also note how the same template parameter elegantly defines the signature of the destructor: noexcept (executeOnException) , since SCOPE_FAIL must be exception safe, and SCOPE_SUCCESS can completely throw an exception in the end, purely out of harm. In my opinion, it is such small architectural details that make C ++ exactly the language that I love. Then everything becomes trivial, like SCOPE_EXIT we define a new macro:
enum class ScopeGuardOnFail {};
template
ScopeGuardForNewException<
typename std::decay::type, true>
operator+(detail::ScopeGuardOnFail, FunctionType&& fn) {
return ScopeGuardForNewException<
typename std::decay::type, true
>(std::forward(fn));
}
#define SCOPE_FAIL \
auto ANONYMOUS_VARIABLE(SCOPE_FAIL_STATE) \
= ::detail::ScopeGuardOnFail() + [&]() noexcept
And similarly for SCOPE_EXIT Let's see how the original 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. And 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 syntax barrier separates us from adding another declarative style to the C ++ idioms.First Person Conclusion
All this leads to certain thoughts about what may await us in the near future. First of all, it struck me that all the links 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 by Alexandrescu about ScopeGuard was published in Dr. Dobbs already in the 2000th year. I want to remind you that Alexandrescu has a reputation as a seer and a prophet, since the Loki library created by him as a demonstration of the concept formed the basis of boost :: mpl, and then almost completely entered the new standard and, long before that, actually set the metaprogramming idioms. On the other hand, Alexandrescu himself has been mainly engaged in the development of the D language recently, where all three of the mentioned constructions are scope exit, scope success and scope failureare part of the syntax of the language and have long taken a strong place in it.Another interesting point is Eric Niebler's talk at the same conference called Ranges for the Standard Library . I want to remind you that ranges is another standard concept of the D language, a further development of the concept of iterators. Moreover, the report itself is actually a translation (from D to C ++) of the wonderful article HSTeoh Component programming with ranges .
Thus, it seems that C ++ has begun 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. Given the lessons of history, the seventeenth year is not boring, stocking up with popcorn, pineapple and hazel grouse.
Literature
Here, links already included in the post are simply collected in one place.- Original audio report
- Link to CppCon 2015 materials
- Alexandrescu report slides
- Link to the original ScopeGuard 2000 article
- Boost documentation :: ScopeExit
- Herb Sutter's suggestion for changing uncaught_exception ()
- Original article on ranges in D , who are interested, a good informal introduction to one aspect of this language