所以我遇到了这个(恕我直言)非常好的想法,使用返回值和异常的复合结构 - Expected<T>
。它克服了传统错误处理方法(异常,错误代码)的许多缺点。
请参阅Andrei Alexandrescu's talk (Systematic Error Handling in C++)和its slides。
异常和错误代码具有基本相同的使用方案,其中函数返回某些内容而不返回内容。另一方面,Expected<T>
似乎只针对返回值的函数。
所以,我的问题是:
Expected<T>
?更新
我想我应该澄清一下我的问题。 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)
,你的代码会是什么样的,它会就地修改字符串并且没有返回值?
答案 0 :(得分:13)
尽管对于那些专注于C-ish语言的人来说,这似乎是新的,但对于那些喜欢支持和类型的语言的人来说,它不是。
例如,在Haskell中你有:
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b
|
读取或的位置和第一个元素(Nothing
,Just
,Left
,Right
)只是一个“标签”。基本上,总和类型只是歧视性联盟。
在这里,Expected<T>
类似Either T Exception
,Expected<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)
自从我看过这个视频后,我一直在思考同样的问题。到目前为止,我没有找到任何令人信服的论据,因为对于我而言,它看起来很荒谬,而且对于清晰度和清洁度。到目前为止,我已经提出了以下建议:
noexcept
。每noexcept
的函数都应该通过try {} catch {} 如果这些陈述成立,那么我们就会有自我记录的易于使用的界面,只有一个缺点:我们不知道在不偷看实现细节的情况下可以抛出哪些异常。
期望对代码施加一些开销,因为如果你的类实现的内容中有一些异常(例如深入私有方法),那么你应该在你的接口方法中捕获它并返回Expected。虽然我认为对于有返回东西的概念的方法是可以忍受的,但我相信它会给设计没有回报价值的方法带来混乱和混乱。除了对我来说,从不应该返回任何东西的东西中归还东西是非常不自然的。
答案 4 :(得分:0)
应使用编译器诊断程序进行处理。许多编译器已经基于某些标准库构造的预期用法发出警告诊断。他们应该发出忽略expected<void>
的警告。