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

时间:2015-10-12 19:17:16

标签: c++ c++11 raii contextmanager

我们的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的术语中,计时器是 contextmanager

现在我们想在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,你几乎不想打扰命名它。

5 个答案:

答案 0 :(得分:2)

编辑:在仔细阅读了戴的评论之后,再多思考一下,我意识到这对于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方法。此外,它使用&#34;正常&#34; class,所以你可以在需要命名的上下文管理器时使用该类,否则使用此函数。您显然可以用非常类似的方式编写with_lock_guard。在那里它更好,因为lock_guard没有你错过的任何会员功能。

所有这一切,我会使用with_lock_guard,还是批准由添加到这样的实用程序中的队友编写的代码?不。一两行额外的代码并不重要;这个功能没有增加足够的效用来证明它自己的存在。 YMMV。

答案 1 :(得分:2)

可以非常接近地模仿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类:实现enterexit方法的traits类-等效于Python的__enter____exit__。一旦将is_detected类型特征引入C ++,该类还可以轻松转发到类类型enter的兼容exitT方法,从而模仿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;
   }
};

答案 2 :(得分:1)

你不需要if( true ),C ++有“匿名范围”,它可以用来限制范围的生命周期,与Python的with或C#using大致相同(好吧,C#也有匿名范围)。

像这样:

doSomething();
{
    Time timer("foo");
    doSomethingElse();
}
doMoreStuff();

只需使用裸花括号。

但是,我不同意你使用RAII语义来检测这样的代码的想法,因为timer析构函数是非平凡的并且具有副设计的副作用。它可能是丑陋和重复的,但我觉得明确地调用名为startTimerstopTimerprintTimer方法使程序更“正确”和自我记录。副作用很糟糕,是吗?

答案 3 :(得分:0)

我最近启动了一个C ++项目,以模仿Python的上下文管理器,因为我将python代码库迁移到了https://github.com/batconjurer/contextual上的C ++。

对于此流程,您需要定义一个从名为IResource的接口派生的资源管理器。在下文中,这称为Timer。在此类中实现enterexit函数。上下文只是需要资源的代码块,因此它是通过匿名函数传递的。

资源管理器希望您实现IData结构,该结构用于存储获取的资源。实际上,它仅保留一个指向IData实例的指针。

对于您的用例,以下是使用C ++ 17编译的示例实现。

#include "include/contextual.h"
#include <ctime>
#include <chrono>
#include <thread>

using namespace Contextual;

namespace Contextual {

    struct IData {
        std::string name;
        std::time_t start_time = std::time(NULL);
        void reset_time() {

            std::cout << "Time before restart: " << start_time << "\n";
            std::time(&start_time);
            std::cout << "Time after restart: " << start_time << "\n";
        };
    };

    class Timer : public IResource<IData> {
    private:
        IData _data;

        void enter() override {
            std::time(&resources->start_time);
        }

        void exit(std::optional<std::exception> e) override {
            double elapsed_time = std::time(NULL) - resources->start_time;
            std::cout << resources->name << " took " << elapsed_time << " seconds.\n";
            if (e) {
                throw e.value();
            }
        }

    public:

        Timer(std::string &&name) : IResource<IData>(_data), _data(IData{name}){};

    };

};

int main(){

    With {
        Timer(std::string("Foo")) + Context{
            [&](IData* time_data) {

                std::chrono::milliseconds sleeptime(5000);
                std::this_thread::sleep_for(sleeptime);     // In place of "Do some work"
                time_data->reset_time();     // In place of "some_mutative_function()"
                std::this_thread::sleep_for(sleeptime);    // In place of "Do some work"
            }
        }
    };


}

我仍在处理一些麻烦的事情(例如,由于IResource仅保留指向它的指针,因此必须将IData结构存储为Timer的实例变量)。当然,C ++异常并不是最好的方法。

答案 4 :(得分:0)

受到戴的回答的启发,我最终得到了以下代码:

#include <iostream>
#include <chrono>

class Timer
{
    std::chrono::high_resolution_clock::time_point startTime;
    public:
    Timer(): startTime(std::chrono::high_resolution_clock::now()){};

    void elapsed()
    {
        auto endTime = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double, std::milli> elapsedTime = endTime - startTime;
        std::cout << elapsedTime.count() << std::endl;
    }
};


int main()
{
    {
        Timer timer=Timer();
        std::cout << "This is some work" << std::endl;
        timer.elapsed();
    }
    return 0;
}

我在python方面比在c ++方面更加流利。我不确定它是否是惯用语言,但对我有用。