你如何使用Alexandrescu的预期<t>和void函数?</t>

时间:2013-02-17 16:34:48

标签: c++ c++11 error-handling runtime-error

所以我遇到了这个(恕我直言)非常好的想法,使用返回值和异常的复合结构 - Expected<T>。它克服了传统错误处理方法(异常,错误代码)的许多缺点。

请参阅Andrei Alexandrescu's talk (Systematic Error Handling in C++)its slides

异常和错误代码具有基本相同的使用方案,其中函数返回某些内容而不返回内容。另一方面,Expected<T>似乎只针对返回值的函数。

所以,我的问题是:

  • 您是否有人在实践中尝试Expected<T>
  • 你如何将这个成语应用于什么都不返回的函数(即void函数)?

更新

我想我应该澄清一下我的问题。 Expected<void>专业化是有道理的,但我对如何使用它更感兴趣 - 一致的使用习惯用法。实现本身是次要的(并且很容易)。

例如,Alexandrescu给出了这个例子(有点编辑):

string s = readline();
auto x = parseInt(s).get(); // throw on error
auto y = parseInt(s); // won’t throw
if (!y.valid()) {
    // ...
}

这段代码以一种自然流动的方式“干净”。我们需要价值 - 我们得到它。但是,对于expected<void>,必须捕获返回的变量并对其执行一些操作(如.throwIfError()或其他),这不是那么优雅。显然,.get()对于无效是没有意义的。

那么,如果你有另一个函数,例如toUpper(s),你的代码会是什么样的,它会就地修改字符串并且没有返回值?

5 个答案:

答案 0 :(得分:13)

尽管对于那些专注于C-ish语言的人来说,这似乎是新的,但对于那些喜欢支持和类型的语言的人来说,它不是。

例如,在Haskell中你有:

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

|读取的位置和第一个元素(NothingJustLeftRight)只是一个“标签”。基本上,总和类型只是歧视性联盟

在这里,Expected<T>类似Either T ExceptionExpected<void>具有Maybe Exception的特殊性,类似于{{1}}。

答案 1 :(得分:12)

  

有没有人试过预期;在实践中?

这很自然,我在看到这个演讲之前就已经习惯了。

  

你如何将这个成语应用于什么都不返回的函数(即void函数)?

幻灯片中显示的表格有一些微妙的含义:

  • 异常绑定到值。
  • 可以按照自己的意愿处理异常。
  • 如果由于某些原因忽略该值,则会禁止该异常。

如果您有expected<void>,则此操作不成立,因为由于没有人对void值感兴趣,因此始终会忽略该异常。我会强迫这一点,因为我会强制阅读Alexandrescus类中的expected<T>,并使用断言和明确的suppress成员函数。不允许从析构函数中重新抛出异常,因此必须使用断言来完成。

template <typename T> struct expected;

#ifdef NDEBUG // no asserts
template <> class expected<void> {
  std::exception_ptr spam;
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)) {}
  expected(expected&& o) : spam(std::move(o.spam)) {}
  expected() : spam() {}

  bool valid() const { return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() {}
};
#else // with asserts, check if return value is checked
      // if all assertions do succeed, the other code is also correct
      // note: do NOT write "assert(expected.valid());"
template <> class expected<void> {
  std::exception_ptr spam;
  mutable std::atomic_bool read; // threadsafe
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {}
  expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {}
  expected() : spam(), read(false) {}

  bool valid() const { read=true; return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() { read=true; }

  ~expected() { assert(read); }
};
#endif

expected<void> calculate(int i)
{
  if (!i) return std::invalid_argument("i must be non-null");
  return {};
}

int main()
{
  calculate(0).suppress(); // suppressing must be explicit
  if (!calculate(1).valid())
    return 1;
  calculate(5); // assert fails
}

答案 2 :(得分:5)

就像Matthieu M.所说,这对于C ++来说是一个相对较新的东西,但对于许多函数式语言来说并不新鲜。

我想在这里加上我的2美分:在我看来,部分困难和差异可以在“程序与功能”方法中找到。我想使用Scala(因为我熟悉Scala和C ++,我觉得它有一个更接近Expected<T>的工具(选项)来说明这种区别。

在Scala中你有Option [T],它是Some(t)或None。 特别是,也可以选择[单位],这在道德上等同于Expected<void>

在Scala中,使用模式非常相似,围绕2个函数构建:isDefined()和get()。但它也有“map()”功能。

我喜欢将“map”视为“isDefined + get”的功能等价物:

if (opt.isDefined)
   opt.get.doSomething

变为

val res = opt.map(t => t.doSomething)

将选项“传播”到结果

我认为,在这种使用和撰写选项的功能风格中,您的问题的答案就在于:

  

那么,如果你有另一个函数,你的代码会是什么样子,比如toUpper(s),它会就地修改字符串并且没有返回值?

就个人而言,我不会修改字符串,或者至少我不会返回任何内容。我认为Expected<T>是一个“功能”概念,需要一个功能模式才能正常工作:toUpper需要返回一个新字符串,或者在修改后返回自己:

auto s = toUpper(s);
s.get(); ...

或者,使用类似Scala的地图

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

如果您不想遵循功能路线,可以使用isDefined / valid并以更加程序化的方式编写代码:

auto s = toUpper(s);
if (s.valid())
    ....

如果你遵循这条路线(也许是因为你需要),有一个“无效与单位”的要点:历史上,void不被认为是一种类型,但是“没有类型”(void foo()被认为是类似帕斯卡程序)。单位(在函数语言中使用)更多地被视为一种意味着“计算”的类型。所以返回一个Option [Unit]确实更有意义,被视为“可选择做某事的计算”。在Expected<void>中,void假定了一个类似的含义:一个计算,当它按预期工作时(没有特殊情况),只是结束(不返回任何内容)。至少,IMO!

因此,使用预期或选项[单位]可以被视为可能产生结果的计算,或者可能不是。链接它们将证明是困难的:

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

不太干净。

Scala中的地图使它更清洁

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

哪个更好,但仍然远非理想。在这里,Maybe monad显然赢了......但那是另一个故事......

答案 3 :(得分:2)

自从我看过这个视频后,我一直在思考同样的问题。到目前为止,我没有找到任何令人信服的论据,因为对于我而言,它看起来很荒谬,而且对于清晰度和清洁度。到目前为止,我已经提出了以下建议:

  • 预期是好的,因为它有值或异常,我们不会强制对每个可抛出的函数使用try {} catch()。因此,将它用于每个具有返回值的投掷函数
  • 每个不抛出的函数都应标有noexcept。每
  • 每个不返回任何未标记为noexcept的函数都应该通过try {} catch {}
  • 包装

如果这些陈述成立,那么我们就会有自我记录的易于使用的界面,只有一个缺点:我们不知道在不偷看实现细节的情况下可以抛出哪些异常。

期望对代码施加一些开销,因为如果你的类实现的内容中有一些异常(例如深入私有方法),那么你应该在你的接口方法中捕获它并返回Expected。虽然我认为对于有返回东西的概念的方法是可以忍受的,但我相信它会给设计没有回报价值的方法带来混乱和混乱。除了对我来说,从不应该返回任何东西的东西中归还东西是非常不自然的。

答案 4 :(得分:0)

应使用编译器诊断程序进行处理。许多编译器已经基于某些标准库构造的预期用法发出警告诊断。他们应该发出忽略expected<void>的警告。