在 C++ 中,像在许多其他语言中一样,我们有不同的方式来表达一个值的存在或不存在。特别是在 C++ 中,我们可以使用以下任何一种:
- 使用
nullptr
对缺勤进行编码。 - 使用智能指针(例如,
shared_ptr
),同样可以测试其是否存在。 std::optional<T>
是库解决方案;如果缺少值,它可以存储类型为T
或std::nullopt
的值。
假设我们决定采用nullptr
方法。在这种情况下,让我们假设我们的域模型定义了一个Person
,它可能有也可能没有一个Address
,反过来,它可以有一个可选的house_name
1 :
1 struct Address {
2 string* house_name = nullptr;
3 };
4
5 struct Person {
6 Address* address = nullptr;
7 };
我们感兴趣的是写一个函数,给定一个人,安全地打印这个人的房屋名称,当然如果它存在的话。在“传统的”C++ 中,我们会这样实现它:
1 void print_house_name(Person* p)
2 {
3 if (p != nullptr &&
4 p->address != nullptr &&
5 p->address->house_name != nullptr) // ugh!
6 cout << *p->address->house_name << endl;
7 }
前面的代码代表了深入对象结构的过程,注意不要访问nullptr
值。相反,这种向下钻取的过程可以通过使用可能单子以函数的方式来表示。
为了构造单子,我们将定义一个新的类型Maybe<T>
。此类型将用作参与下钻过程的临时对象:
1 template <typename T> struct Maybe {
2 T* context;
3 Maybe(T *context) : context(context) { }
4 };
到目前为止,Maybe
看起来像一个指针容器,没什么令人兴奋的。它也不是很有用,因为给定一个Person* p
,我们不能产生一个Maybe(p)
,因为我们不能从构造器中传递的参数推导出类模板参数。在这种情况下,我们还创建了一个 helper 全局函数,因为函数实际上可以推导出模板参数:
1 template <typename T> Maybe<T> maybe(T* context)
2 {
3 return Maybe<T>(context);
4 }
现在,我们想要做的是给Maybe
一个成员函数
- 如果
context != nullptr
,则更深地钻入对象;或者 - 如果上下文实际上是
nullptr
,则什么也不做
“向下钻取”的过程被封装到模板参数Func
中,如下所示:
1 template <typename Func>
2 auto With(Func evaluator)
3 {
4 return context != nullptr ? maybe(evaluator(context)) : nullptr;
5 }
前面是高阶函数的一个例子,也就是取函数的函数。 2 我们创建的这个函数采用了另一个名为evaluator
的函数,假设当前上下文为非空,可以在上下文中调用这个函数,并返回一个可以包装在另一个Maybe
中的指针。这个技巧允许链接With()
呼叫。
现在,以类似的方式,我们可以创建另一个成员函数,这次只需调用context
上的给定函数,而不改变上下文本身:
1 template <typename TFunc>
2 auto Do(TFunc action)
3 {
4 if (context != nullptr) action(context);
5 return *this;
6 }
我们完事了。我们现在可以做的是重新定义我们的print_house_name()
函数如下:
1 void print_house_name(Person* p)
2 {
3 auto z = maybe(p)
4 .With([](auto x) { return x->address; })
5 .With([](auto x) { return x->house_name; })
6 .Do([](auto x) { cout << *x << endl; });
7 }
这里有几点需要注意。首先,我们设法创建了一个流畅的接口,即一个可以将函数调用一个接一个链接起来的设置。这种说法是有道理的,因为每个操作符(With
、Do
等)。)返回*this
或者一个新的Maybe<T>
。同样值得注意的是,下钻过程是如何在每一个转折点被 lambda 函数封装的。
正如您可能猜到的,前面的方法确实有性能成本,尽管这些成本很难预测,并且取决于编译器优化代码的能力。它也远非完美,因为我很乐意省略[](auto x)
部分,以支持一些速记符号。理想情况下,类似于maybe(p).With{it->address}
的东西会很好。T33
Footnotes 1
房子的名字是真实存在的(至少在英国是这样的):当你买了一座城堡,它的地址不是“伦敦路 123 号”,而只是“蒙特菲奥里城堡”,这就是它的地址。你可以猜到,并不是所有的房子都有名字,这就解释了为什么这个字段是可选的。
严格地说,高阶函数要么接受一个函数作为一个或多个参数,要么返回一个函数(或两者都有)。
例如,Kotlin 和 Swift 编程语言支持这种方法。如果没有必要,这两种语言都允许程序员避免额外的 lambda 函数仪式。这包括省略参数、捕获列表和返回值,以及使用花括号,而不是圆括号,这让您可以简单地打开一个事实上的作用域,并放置所有要由 lambda 执行的语句。