Declarative Control Flow
Prepared for CppCon
Bellevue, WA, Sep 20–25, 2015
Andrei Alexandrescu, Ph.D.
[email protected]© 2015- Andrei Alexandrescu. Do not redistribute. 1 / 41
Introduction
There is no introduction slide
© 2015- Andrei Alexandrescu. Do not redistribute. 2 / 41
Challenge
• Define a transactional file copy function
• Either succeed
• . . . or fail successfully
© 2015- Andrei Alexandrescu. Do not redistribute. 3 / 41
Cheat Sheet
• From boost::filesystem:
void copy_file(
const path& from,
const path& to
);
© 2015- Andrei Alexandrescu. Do not redistribute. 4 / 41
Implementation
void copy_file_transact(const path& from,
const path& to) {
bf::path t = to.native() + ".deleteme";
try {
bf::copy_file(from, t);
bf::rename(t, to);
} catch (...) {
::remove(t.c_str());
throw;
}
}
© 2015- Andrei Alexandrescu. Do not redistribute. 5 / 41
Meh
• We now have explicit control flow
◦ Not dedicated to core logic
• Of 8 lines, only 4 do “work”
• Y u play baseball?
© 2015- Andrei Alexandrescu. Do not redistribute. 6 / 41
Composition
• Define a transactional file move function
• Either succeed
• . . . or fail successfully
© 2015- Andrei Alexandrescu. Do not redistribute. 7 / 41
Implementation
void move_file_transact(const path& from,
const path& to) {
try {
bf::copy_file_transact(from, to);
bf::remove(from);
} catch (...) {
::remove(to.c_str());
throw;
}
}
© 2015- Andrei Alexandrescu. Do not redistribute. 8 / 41
Ehm
• Same issues
• Also, functions that throw don’t compose all
that well
• Oops, there’s a bug
© 2015- Andrei Alexandrescu. Do not redistribute. 9 / 41
Implementation (fixed)
void move_file_transact(const path& from,
const path& to) {
bf::copy_file_transact(from, to);
try {
bf::remove(from);
} catch (...) {
::remove(to.c_str());
throw;
}
}
© 2015- Andrei Alexandrescu. Do not redistribute. 10 / 41
What Tools Do We Have?
• RAII tenuous
• ScopeGuard tenuous
• Composition only makes it worse
◦ Series/nesting of try/catch
◦ Urgh
• Meantime in CppCoreStandards:
“E.18: Minimize the use of explicit try/catch”
© 2015- Andrei Alexandrescu. Do not redistribute. 11 / 41
Suddenly. . .
goto fail; doesn’t look that bad
anymore, eh?
© 2015- Andrei Alexandrescu. Do not redistribute. 12 / 41
Explicit Control Flow =
Fail
© 2015- Andrei Alexandrescu. Do not redistribute. 13 / 41
Declarative Programming
• Focus on stating needed accomplishments
• As opposed to describing steps
• Control flow typically minimal/absent
• Execution is implicit, not explicit
• Examples: SQL, regex, make, config,. . .
• Let’s take a page from their book!
© 2015- Andrei Alexandrescu. Do not redistribute. 14 / 41
Surprising Insight
• Consider bona fide RAII with destructors:
X States needed accomplishment?
X Implicit execution?
X Control flow minimal?
• RAII is declarative programming!
© 2015- Andrei Alexandrescu. Do not redistribute. 15 / 41
More RAII: ScopeGuard
• Also declarative
• Less syntactic baggage than cdtors
• Flow is “automated” through placement
• Macro SCOPE_EXIT raises it to
pseudo-statement status
© 2015- Andrei Alexandrescu. Do not redistribute. 16 / 41
Pseudo-Statement (old hat!)
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 ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) \
= ::detail::ScopeGuardOnExit() + [&]()
© 2015- Andrei Alexandrescu. Do not redistribute. 17 / 41
Preprocessor Trick (old hat!)
#define CONCATENATE_IMPL(s1, s2) s1##s2
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) \
CONCATENATE(str, __COUNTER__)
#else
#define ANONYMOUS_VARIABLE(str) \
CONCATENATE(str, __LINE__)
#endif
© 2015- Andrei Alexandrescu. Do not redistribute. 18 / 41
Use (old hat!)
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); };
... use fd and buf ...
}
(if no “;” after lambda, error message is fail)
© 2015- Andrei Alexandrescu. Do not redistribute. 19 / 41
Painfully Close to Ideal!
haction1i
SCOPE_EXIT { hcleanup1i };
SCOPE_FAIL { hrollback1i }; // nope
haction2i
SCOPE_EXIT { hcleanup2i };
SCOPE_FAIL { hrollback2i }; // nope
© 2015- Andrei Alexandrescu. Do not redistribute. 20 / 41
One more for completeness
haction i
SCOPE_SUCCESS { hcelebrate i };
hnext i
• Powerful flow-declarative trifecta!
• Do not specify flow
• Instead declare circumstances and goals
© 2015- Andrei Alexandrescu. Do not redistribute. 21 / 41
May Will Has become
100% portable:
C++17 has it!
http://isocpp.org/files/papers/N4152.pdf
© 2015- Andrei Alexandrescu. Do not redistribute. 22 / 41
N4152
• “When in doubt, replace bool with int”
namespace std {
bool uncaught_exception(); // old and bad
int uncaught_exceptions(); // new and rad
}
© 2015- Andrei Alexandrescu. Do not redistribute. 23 / 41
Using std::uncaught_exceptions
class UncaughtExceptionCounter {
int getUncaughtExceptionCount() noexcept;
int exceptionCount_;
public:
UncaughtExceptionCounter()
: exceptionCount_(std::uncaught_exceptions()) {
}
bool newUncaughtException() noexcept {
return std::uncaught_exceptions()
> exceptionCount_;
}
};
© 2015- Andrei Alexandrescu. Do not redistribute. 24 / 41
Layering
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_();
}
}
};
© 2015- Andrei Alexandrescu. Do not redistribute. 25 / 41
Icing
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));
}
© 2015- Andrei Alexandrescu. Do not redistribute. 26 / 41
Cake Candles
#define SCOPE_FAIL \
auto ANONYMOUS_VARIABLE(SCOPE_FAIL_STATE) \
= ::detail::ScopeGuardOnFail() + [&]() noexcept
© 2015- Andrei Alexandrescu. Do not redistribute. 27 / 41
Back to Example
© 2015- Andrei Alexandrescu. Do not redistribute. 28 / 41
Transactional Copy
void copy_file_transact(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);
}
• All code does work
© 2015- Andrei Alexandrescu. Do not redistribute. 29 / 41
Transactional Move
void move_file_transact(const path& from,
const path& to) {
bf::copy_file_transact(from, to);
SCOPE_FAIL { ::remove(to.c_str()); };
bf::remove(from);
}
• Simpler than the no-exceptions version!
© 2015- Andrei Alexandrescu. Do not redistribute. 30 / 41
Please Note
Only SCOPE_SUCCESS
may throw
© 2015- Andrei Alexandrescu. Do not redistribute. 31 / 41
Postconditions
int string2int(const string& s) {
int r;
SCOPE_SUCCESS {
assert(int2string(r) == s);
};
...
return r;
}
© 2015- Andrei Alexandrescu. Do not redistribute. 32 / 41
Changing of the Guard
void process(char *const buf, size_t len) {
if (!len) return;
const auto save = buf[len - 1];
buf[len - 1] = 255;
SCOPE_EXIT { buf[len - 1] = save; };
for (auto p = buf;;) switch (auto c = *p++) {
...
}
}
© 2015- Andrei Alexandrescu. Do not redistribute. 33 / 41
Scoped Changes
bool g_sweeping;
void sweep() {
g_sweeping = true;
SCOPE_EXIT { g_sweeping = false; };
auto r = getRoot();
assert(r);
r->sweepAll();
}
© 2015- Andrei Alexandrescu. Do not redistribute. 34 / 41
No RAII Type? No Problem!
void fileTransact(int fd) {
enforce(flock(fd, LOCK_EX) == 0);
SCOPE_EXIT {
enforce(flock(fd, LOCK_UN) == 0);
};
...
}
• No need to add a type for occasional RAII
idioms
© 2015- Andrei Alexandrescu. Do not redistribute. 35 / 41
Remarks
• All examples taken from production code
• Declarative focus
◦ Declare contingency actions by context
• SCOPE_* more frequent than try in new code
• The latter remains in use for actual handling
• Flattened flow
• Order still matters
© 2015- Andrei Alexandrescu. Do not redistribute. 36 / 41
Only detail left:
std::uncaught_exceptions()
© 2015- Andrei Alexandrescu. Do not redistribute. 37 / 41
Implemented and
maintained on ALL
major compilers
© 2015- Andrei Alexandrescu. Do not redistribute. 38 / 41
Credits
• Evgeny Panasyuk: compiler-specific bits
github.com/panaseleus/stack_unwinding
• Daniel Marinescu: folly implementation
github.com/facebook/folly
• Herb: got it in C++17
• Michael: Implemented it in Visual Studio
2015
• Ville Voutilainen implemented it for gcc
© 2015- Andrei Alexandrescu. Do not redistribute. 39 / 41
Summary
© 2015- Andrei Alexandrescu. Do not redistribute. 40 / 41
Summary
There is no summary slide
© 2015- Andrei Alexandrescu. Do not redistribute. 41 / 41