我们的Python代码库具有与度量相关的代码,如下所示:
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
在Python术语中,计时器是一个上下文管理器。
现在我们想在C ++中实现相同的东西,同样好的语法。不幸的是,C ++没有with
。所以“明显的”成语将是(经典的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
}
但这是非常丑陋的句法盐:它的行数比需要的长很多,我们不得不为我们的“未命名”计时器引入一个名称t
(如果我们忘记这个名字,代码就会默默地破坏)......这只是丑陋的。
人们习惯用C ++处理“上下文管理器”的一些句法习惯用法是什么?
我已经想到了这个滥用的想法,它减少了行数,但没有摆脱名称t
:
// give Timer an implicit always-true conversion to bool
if (auto t = Timer("foo")) {
do some work
}
或者这个建筑怪物,我甚至不相信自己正确使用:
Timer("foo", [&](auto&) {
do some work
});
Timer("bar", [&](auto& named_timer) {
do some work
named_timer.some_mutative_method();
do some more work
});
其中Timer
的构造函数实际调用给定的lambda(带有参数*this
)并且一次性完成日志记录。
但是,这些想法似乎都不是“最佳实践”。帮帮我吧!
另一种表达问题的方法可能是:如果你是从头开始设计std::lock_guard
,你会怎么做才能消除尽可能多的样板? lock_guard
是一个完美的上下文管理器的例子:它是一个实用程序,它本质上是RAII,你几乎不想打扰命名它。
可以非常接近地模仿Python语法和语义。以下测试用例编译并且与Python中的语义大致相似:
// 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";
};
}
}
那是给定的
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";
}
};
with
自由函数将所有工作传递给with_impl
类:
template <typename T, typename... Ts>
with_impl<T> with(Ts &&... args) {
return with_impl<T>(std::forward<Ts>(args)...);
}
我们怎么去那里?首先,我们需要一个context_manager
类:实现enter
和exit
方法的traits类 - 相当于Python的__enter__
和__exit__
。一旦is_detected
类型特征进入C ++,这个类也可以轻松转发到类型为enter
的兼容exit
和T
方法,从而更好地模仿Python的语义。就目前而言,上下文管理器非常简单:
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> {};
让我们看看这个类如何专门用于包装Resource
对象,或std::FILE *
。
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({}); }
};
核心功能的实现是在with_impl
类型。请注意套件中的异常处理(第一个lambda)和exit
函数是如何模仿Python行为的。
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;
}
};
编辑:在仔细阅读戴的评论后,再多想一想,我意识到这对于C ++ RAII来说是一个糟糕的选择。为什么?因为你正在登录析构函数,这意味着你正在做io,而io可以抛出。 C ++析构函数不应该发出异常。使用python,写一个投掷__exit__
也不一定很棒,它可能会导致你在地板上放弃你的第一个异常。但是在python中,你明确地知道上下文管理器中的代码是否引起了异常。如果它导致异常,您可以省略__exit__
中的日志记录并通过异常。如果您有一个不会冒险退出的上下文管理器,我会在下面留下我的原始答案。
C ++版本比python版本长2行,每个花括号一个。如果C ++只比python长两行,那就做得很好。上下文管理器是针对这一特定事物而设计的,RAII更为通用,并提供了严格的功能超集。如果你想知道最佳实践,你已经找到了它:拥有一个匿名范围并在开头创建对象。这是惯用的。您可能会发现它来自python,但在C ++世界中它很好。同样地,来自C ++的人会在某些情况下发现上下文管理器难看。 FWIW我专业地使用这两种语言,这根本不会打扰我。
也就是说,我将为匿名上下文管理器提供更清晰的方法。使用lambda构造Timer并立即让它破坏的方法非常奇怪,所以你应该怀疑是正确的。更好的方法:
template <class F>
void with_timer(const std::string & name, F && f) {
Timer timer(name);
f();
}
用法:
with_timer("hello", [&] {
do something;
});
这相当于匿名上下文管理器,在某种意义上,除了构造和销毁之外,不能调用Timer的任何方法。此外,它使用“普通”类,因此您可以在需要命名上下文管理器时使用该类,否则使用此函数。您显然可以用非常类似的方式编写with_lock_guard。在那里,它甚至更好,因为lock_guard没有你错过的任何成员函数。
所有这一切,我会使用with_lock_guard,还是批准由添加到这样的实用程序中的队友编写的代码?不。一两行额外的代码无关紧要;此函数没有添加足够的实用程序来证明它自己的存在。因人而异。
你不需要if( true )
,C ++有“匿名范围”可以用来限制范围的生命周期,就像Python的with
或C#s using
一样(嗯,C#也有匿名范围)。
像这样:
doSomething();
{
Time timer("foo");
doSomethingElse();
}
doMoreStuff();
只需使用裸花括号。
但是,我不同意你使用RAII语义来设计这样的代码的想法,因为timer
析构函数是非平凡的并且具有副设计的副作用。它可能是丑陋和重复的,但我觉得明确地称为startTimer
,stopTimer
和printTimer
方法使程序更“正确”和自我记录。副作用很糟糕,是吗?