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
上下文中对其进行评估来检测。要访问实际数据(假设它存在),您可以使用一元 *
或 ->
.
你还记得本科生讲授算法时,有一个 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
上下文中对其进行评估来检测。要访问实际数据(假设它存在),您可以使用一元 *
或 ->
.