std::monostate ≠ “空”类型
C++17 存在一个不起眼的类型 std::monostate
,引入背景是 std::variant
需要允许默认构造,而如果它所有的候选类型都不支持默认构造,那就可以将 std::monostate
作为第一个候选类型来避免无效状态。
一个小例子:
struct S {
explicit S(int) {}
};
// Error: the first alternative is not default constructible.
std::variant<S> v;
// OK
std::variant<std::monostate, S> w;
因此,std::monostate
也被放到了 <variant>
,定义很简单:
struct monostate {};
许多人容易把它当成是一个“空”类型,但从严格意义上来讲,它同 void
一样,是一个单值类型。
我在 C++ Adventures: Types 一文中从语言设计的层面上系统地介绍过类型的分类,C++ 并不像 Rust、Haskell、Scala 等语言那样存在真正的“空”类型。真正的“空”类型无法表示任何值,也无法被创建,所以一个以“空”类型作为参数的函数永远也无法被调用。
std::monostate
类型可以被创建,但该类型只能够包含一个值,因此它也是一个 Unit Type。单值类型只能表示一个状态,这种特殊性使其成为一个没有意义的类型。std::nullptr_t
也是这样的一种类型,不过因为它能够隐式转换成任意的指针类型,并且可以被流输出,误用的几率较大,所以不适合用来作为通用的无意义表示类型。
std::monostate
支持默认构造、拷贝构造和所有的比较操作,是一个常规类型,而 void
是由关键字声明的内置类型,它是一个不完整类型(Incomplete Type),无法创建对象。因此,虽然都是 Unit Type,但 std::monostate
存在一些特殊的使用场景。
第一,你可以用它来测试模板容器的健壮性。因为 std::monostate
不支持输出、运算、隐式转换等功能,也不包含任何成员,是最简单的常规类型,所以可以尝试以其作为模板参数实例化容器。如果没有报错,就表示容器的设计合理,没有强依赖。
第二,你可以将它作为可选模板参数的默认类型。
template<typename ExtraInfo = std::monostate>
class Data {
// ...
[[no_unique_address]] ExtraInfo info;
};
用户可以选择是否传递额外的信息,std::monostate
作为默认类型,不会误用、不占空间、也不会和其他类型产生冲突。
此时,往往需要结合 [[no_unique_address]]
使用,以优化基类子对象的占用空间,防止这个无意义的成员增加对象的大小。这方面可以参考文章:那些值得使用的 C++ Attributes。
第三,你可以将它作为无意义成员的替代类型。
template<bool Debug>
class S {
public:
void log_info(const std::string& msg) {
if constexpr (Debug) {
log_.info(msg);
}
}
private:
[[no_unique_address]] std::conditional_t<Debug, Log, std::monostate> log_;
};
这样,在非调试状态下,代码依旧可以正常编译,却不会具备真实的功能。
第四,作为线性递归的 Root 类型。在 C++ Generative Metaprogramming 一书的第 6.3.1 小节,单独写了一个 struct empty_type{}
作为线性递归继承的默认结束条件,那是为了通用性考虑。如果在 C++17 之后也有类似的需求,那么便可以直接使用已经存在的 std::monostate
类型。
也正是因为 std::monostate
的这种通用性,C++26 把它添加到了 <utility>
,以避免不需要 std::variant
时引入额外的内容。同时,为了保证代码的向后兼容性,<variant>
中也并没有直接将其移除。