37

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

请参阅Andrei Alexandrescu 的演讲(C++ 中的系统错误处理)及其幻灯片

异常和错误代码具有基本相同的使用场景,具有返回某些内容的函数和不返回某些内容的函数。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()对 void 没有意义。

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

4

5 回答 5

13

尽管对于只关注 C-ish 语言的人来说它可能看起来很新鲜,但对于我们这些喜欢支持 sum 类型的语言的人来说,它不是。

例如,在 Haskell 中,您有:

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

其中|读取和第一个元素(Nothing, Just, Left, Right)只是一个“标签”。本质上 sum-types 只是有区别的 unions

在这里,你会Expected<T>是这样的:Either T Exception具有Expected<void>类似于Maybe Exception.

于 2013-02-17T16:52:17.893 回答
13

你们中的任何人都尝试过预期吗?在实践中?

这很自然,甚至在我看到这个演讲之前我就使用了它。

您如何将这个习惯用法应用于不返回任何内容的函数(即 void 函数)?

幻灯片中呈现的表格有一些微妙的含义:

  • 异常绑定到该值。
  • 可以根据需要处理异常。
  • 如果由于某些原因忽略了该值,则会抑制异常。

如果你有,这不成立expected<void>,因为没有人对这个void值感兴趣,所以总是忽略异常。我会强制这样做,因为我会强制从expected<T>Alexandrescus 类中读取,带有断言和显式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
}
于 2013-02-17T17:41:10.110 回答
5

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

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

在 Scala 中,您有 Option[T],它是 Some(t) 或 None。特别是,也可能有 Option[Unit],它在道德上等价于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(s) 需要返回一个新字符串,或者在修改后返回自身:

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 vs. unit”的观点:从历史上看,void 不被视为一种类型,但“无类型”(void foo() 被视为类似于 Pascal程序)。单元(在函数式语言中使用)更多地被视为一种类型,意思是“计算”。所以返回一个 Option[Unit] 确实更有意义,被视为“一个可以选择做某事的计算”。在 中Expected<void>,void 具有类似的含义:当它按预期工作时(没有例外情况),计算就结束了(什么也不返回)。至少,海事组织!

因此,使用 Expected 或 Option[Unit] 可以被视为可能产生结果的计算,也可能不产生结果。将它们链接起来会很困难:

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 中的 Map 让它更干净一点

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

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

于 2013-02-28T10:12:34.850 回答
2

自从看了这个视频后,我一直在思考同样的问题。到目前为止,我还没有找到任何令人信服的论据来支持 Expected,对我来说这看起来很荒谬,而且不利于清晰和干净。到目前为止,我提出了以下几点:

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

如果这些陈述成立,那么我们已经自我记录了易于使用的接口,只有一个缺点:如果不查看实现细节,我们不知道会抛出哪些异常。

预期会给代码带来一些开销,因为如果您在类实现的内部有一些异常(例如,在私有方法内部),那么您应该在接口方法中捕获它并返回预期。虽然我认为对于具有返回某些概念的方法来说这是可以容忍的,但我相信它会给设计上没有返回值的方法带来混乱和混乱。此外,对我来说,从不应该返回任何东西的东西中返回东西是很不自然的。

于 2013-02-27T06:21:30.673 回答
0

它应该通过编译器诊断来处理。许多编译器已经根据某些标准库结构的预期用途发出警告诊断。他们应该发出警告以忽略expected<void>.

于 2019-02-09T05:08:03.870 回答