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> 中也并没有直接将其移除。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.