在C++中实现"contextmanager"的最佳实践+语法

Quu*_*one 9 c++ raii contextmanager c++11

我们的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
Run Code Online (Sandbox Code Playgroud)

在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
}
Run Code Online (Sandbox Code Playgroud)

但这是非常丑陋的句法盐:它的行数比它需要的长很多,我们必须t为我们的"未命名"计时器引入一个名称(如果我们忘记了这个名字,代码就会默默地破坏)......这只是丑陋的.

人们习惯用C++处理"上下文管理器"的一些句法习惯用法是什么?


我已经想到了这个滥用的想法,它减少了行数,但没有删除名称t:

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

或者这个建筑怪物,我甚至不相信自己正确使用:

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

其中构造函数Timer实际调用给定的lambda(带参数*this)并一次性完成日志记录.

但是,这些想法似乎都不是"最佳实践".帮帮我吧!


另一种表达问题的方法可能是:如果你是std::lock_guard从头开始设计,你将如何做到尽可能消除尽可能多的样板? lock_guard是一个完美的上下文管理器的例子:它是一个实用程序,它本质上是RAII,你几乎不想打扰命名它.

Rei*_*ica 11

可以非常接近地模仿 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";
      };
   }
}
Run Code Online (Sandbox Code Playgroud)

那是给的

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";
   }
};
Run Code Online (Sandbox Code Playgroud)

with免费功能通过了所有工作的with_impl类:

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

我们怎么去那里?首先,我们需要一个context_manager类:实现enterexit方法的traits 类——相当于Python 的__enter____exit__。一旦is_detectedtype trait 被引入 C++,这个类也可以很容易地转发到类 type的 compatibleenterexit方法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> {};
Run Code Online (Sandbox Code Playgroud)

让我们看看这个类如何专门用于包装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({}); }
};
Run Code Online (Sandbox Code Playgroud)

核心功能的实现在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;
   }
};
Run Code Online (Sandbox Code Playgroud)

  • 回顾一下:如果您认为上面答案中的库代码“丑陋”,请看看“unique_ptr”在您选择的任何 C++ 库实现中的外观。它使这段代码看起来很棒:) (3认同)

Dai*_*Dai 8

你不需要if( true ),C++ 有“匿名作用域”,可以用来限制作用域的生命周期,就像 Pythonwith或 C#一样using(当然,C# 也有匿名作用域)。

就像这样:

doSomething();
{
    Time timer("foo");
    doSomethingElse();
}
doMoreStuff();
Run Code Online (Sandbox Code Playgroud)

只需使用裸花括号即可。

然而,我不同意你使用 RAII 语义来检测这样的代码的想法,因为timer析构函数并不简单,并且设计上有副作用。它可能很丑陋且重复,但我觉得显式调用 namestartTimerstopTimer方法printTimer使程序更加“正确”和自记录。副作用很糟糕,是吗?

  • 我不明白为什么你认为 RAII 在这里不合适。所有 RAII 析构函数都非常重要,因为它们必须释放一些资源。如果它很琐碎,那么 RAII 类就会是多余的。 (3认同)
  • try-catch 将阻止异常逃逸析构函数。事实证明,boost 提供了一个基于 RAII 的计时器(记录),请参阅:http://www.boost.org/doc/libs/1_59_0/libs/timer/doc/cpu_timers.html。也许OP可以只使用boost的类。 (3认同)
  • @pcarter 实际上,我认为 Dai 说得很好。在这种情况下,析构函数正在执行日志记录,这意味着 io,这意味着它实际上可能会抛出异常。抛出析构函数是不好的。 (2认同)

Nir*_*man 6

编辑:在更仔细地阅读 Dai 的评论并思考更多之后,我意识到这对于 C++ RAII 来说是一个糟糕的选择。为什么?因为你登录的是析构函数,这意味着你在做io,io可以抛出。C++ 析构函数不应发出异常。使用 python,编写一个 throwing__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();
}
Run Code Online (Sandbox Code Playgroud)

用法:

with_timer("hello", [&] {
    do something;
});
Run Code Online (Sandbox Code Playgroud)

这相当于匿名上下文管理器,因为除了构造和销毁之外,不能调用 Timer 的任何方法。此外,它使用“普通”类,因此您可以在需要命名上下文管理器时使用该类,否则使用此函数。您显然可以以非常相似的方式编写 with_lock_guard。那里更好,因为 lock_guard 没有您错过的任何成员函数。

综上所述,我会使用 with_lock_guard,还是批准由添加到此类实用程序中的队友编写的代码?不。一两行额外的代码并不重要;这个函数没有增加足够的效用来证明它自己的存在。天啊。