C++中“Nil”的概念

The concept of `Nil` in C++

你还记得本科生讲授算法时,有一个 Nil 的概念非常方便,任何东西都可以赋值给它或与之比较。 (顺便说一下,我从来没有读过计算机科学的本科生。)在 Python 中我们可以使用 None;在 Scala 中有 Nothing (如果我理解正确的话,它是所有内容的子对象)。但我的问题是,我们如何在 C++ 中拥有 Nil?以下是我的想法。

我们可以使用 Singleton Design Pattern 定义单例对象,但我目前的印象是你们中的大多数人一想到这个就会畏缩。

或者我们可以定义全局或静态。

我的问题是,在这两种情况下,我想不出一种方法可以将任何类型的任何变量分配给 Nil,或者能够比较任何类型的任何对象Nil。 Python 的 None 很有用,因为 Python 是动态类型的; Scala 的 Nothing(不要与 Scala Nil 混淆,后者表示空列表)优雅地解决了这个问题,因为 Nothing 是所有内容的子对象。那么在 C++ 中是否有一种优雅的方式 Nil 呢?

不,C++ 中没有通用的 nil 值。逐字地,C++ 类型是不相关的并且不共享公共值。

您可以通过使用继承实现一些形式的可共享值,但您必须明确地这样做,并且只能对用户定义的类型这样做。

在C++11中有一个值nullptr,它可以赋值给任何指针类型。

如果它不是指针类型,则它必须存在。对于特定值,您可以定义特殊的 "I-don't-exist" 值,但没有通用的解决方案。

如果它可能不存在,您的选择是:

  • 指向它并使用 nullptr 指示不存在的值,或者
  • 保留一个单独的标志来指示存在或不存在(这是 boost::optional 为您所做的),或者
  • 为这个特定的用法定义一个特殊的值来表示一个不存在的值(注意这并不总是可能的。)

C++ 遵循不为不使用的东西付费的原则。例如,如果你想要一个 32 位整数来存储整个范围的 32 位值 一个关于它是否为 Nil 的附加标志,这将需要超过 32 位的贮存。当然你可以创建聪明的 类 来表示这种行为,但它不可用 "out of the box".

如果不依赖封装您的类型的容器(例如 boost::optional),您就无法在 C++ 中使用它。

的确,有多种原因,因为 C++ 是:

  • 静态类型语言:一旦你用一个类型定义了一个变量,它就会坚持那个类型(这意味着你不能分配一个值None,它不在每个类型可以代表的范围内,你可以用其他动态类型的语言来做,比如 Python)
  • 具有内置 [1] 类型且其类型没有公共基础对象的语言:您不能在所有类型的公共对象中具有 None 加上 "All the values that this type could represent otherwise" 的语义.

[1] 不像 Java 内置类型,它们都有相应的 class 继承自 java.lang.Object

您描述为 Nil 的有两个相关概念:unit type and the option type

单位类型

这就是 Python 中的 NoneType 和 C++ 中的 nullptr_t,它只是一种特殊类型,具有传达这种特定行为的单个值。由于 Python 是动态类型的,因此 any 对象可以与 None 进行比较,但在 C++ 中我们只能对指针执行此操作:

void foo(T* obj) {
    if (obj == nullptr) {
        // Nil
    }
}

这在语义上与 python 的相同:

def foo(obj):
    if foo is None:
        # Nil

选项类型

Python 没有(或不需要)这样的功能,但所有 ML 系列都有。这可以通过 boost::optional 在 C++ 中实现。这是一个 类型安全 封装的想法,即特定对象可以 可能 有值,也可以没有。这个想法在函数族中比在 C++ 中更具表现力:

def foo(obj: Option[T]) = obj match {
    case None => // "Nil" case
    case Some(v) => // Actual value case
}

虽然在 C++ 中也很容易理解,一旦你看到它:

void foo(boost::optional<T> const& obj) {
    if (obj) {
        T const& value = *obj;
        // etc.
    }
    else {
        // Nil
    }
}

这里的优点是选项类型是值类型,你可以很容易地表达一个"value"什么都没有(例如你的optional<int*>可以存储nullptr作为一个值,与 "Nil" 分开)。这也适用于任何类型,而不仅仅是指针——只是您必须为添加的功能付费。 optional<T> 将大于 T 并且使用起来更昂贵(尽管只有一点点,尽管那一点可能很重要)。

C++ 零

我们可以将这些想法组合在一起,在 C++ 中实际制作一个 Nil,至少一个可以使用指针和可选值。

struct Nil_t { };
constexpr Nil_t Nil;

// for pointers
template <typename T>
bool operator==(const T* t, Nil_t ) { return t == nullptr; }

template <typename T>
bool operator==(Nil_t, const T* t ) { return t == nullptr; }

// for optionals, rhs omitted for brevity
template <typename T>
bool operator==(const boost::optional<T>& t, Nil_t ) { return !t; }

(请注意,我们甚至可以将其推广到任何实现 operator!

template <typename T>
bool operator==(const T& t, Nil_t ) { return !t; }

,但最好将我们自己限制在明确的情况下,我喜欢指针和可选值给你的明确性)

因此:

int* i = 0;
std::cout << (i == Nil); // true
i = new int(42);
std::cout << (i == Nil); // false

在大多数这些语言中,变量不是作为对象的名称来实现的,而是作为对象的句柄来实现的。

可以将此类句柄设置为 "hold nothing",而额外开销很少。这类似于 C++ 指针,其中 nullptr 状态对应于 "pointing to nothing".

通常这样的语言在垃圾收集上做的很充分。垃圾收集和数据的强制间接引用在实践中都会对性能产生重大影响,并且它们会限制您可以执行的操作类型。

当您在此类语言中使用变量时,您必须首先'follow the pointer'获得真实的对象。

在 C++ 中,变量名通常指的是所讨论的实际对象,而不是对它的引用。在 C++ 中使用变量时,可以直接访问它。一个额外的状态(对应于"nothing")将需要在每个变量中有额外的开销,而C++的原则之一是你不为你使用的东西付费。

有多种方法可以在 C++ 中创建 "nullable" 数据类型。这些不同于使用原始指针、使用智能指针或使用类似 std::experimantal::optional 的东西。它们都有一个 "there is nothing there" 的状态,通常可以通过获取对象并在 bool 上下文中对其进行评估来检测。要访问实际数据(假设它存在),您可以使用一元 *->.