Something I implemented today: “is void”

[Edited to add pre-publication link to next draft of P2392, revision 2, and correct iterator comparison]

Brief background

As I presented at CppCon 2021 starting at 11:15, I’m proposing is (a general type or value query) and as (a general cast, for only the safe casts) for C++ evolution. The talk, and the ISO C++ evolution paper P2392 it’s based on, explained why I hope that is and as can provide a general mechanism to power pattern matching with inspect, while conversely also liberating the power of pattern matching beyond just inspect for use generally in the language (e.g., in requires clauses, in general code). Here’s the key slide from last year, that I cited again in this year’s talk:

Note this isn’t about “making it look pretty.” is and as do lead to simpler and prettier code, but whereas human programmers love clear and consistent spellings, generic code demands that consistency. Here’s the key 1-min clip from last year, summarizing the argument in a nutshell:

Today: Divergent emptiness

In that vein, today I was catching up with some cppfront PRs, and Drew Gross pointed out that as I’ve begun implementing is and as in Cpp2 syntax, one thing didn’t work as Drew expected:

is std::nullopt_t doesn’t appear to match an empty optional

Drew Gross in cppfront PR #5

That got me thinking.

First, that this wasn’t supported in the current P2392 was intentional, because nullopt_t isn’t a “real type”… it’s a signal for “empty / no value” which until now wasn’t covered in P2392. Recall that is and as are related, including that if is T succeeds it means that a dynamic as T cast to the same type will succeed. But that’s not true for nullopt_t, which is optional‘s way of signaling an empty state; you can’t cast to “no type.”

Still, this got me thinking that testing for “empty” could be useful. And if we do provide an is test for an empty optional, it makes sense for there not to be an as cast for that, which simplifies how we think about it. And it should be spelled generically in a way that works equally for other kind of empty things, so we wouldn’t want to spell it is nullopt_t because that “empty state” name is specific to optional only. It is one of many existing divergent ad-hoc spellings we’ve added for “empty state” (just like we had lots of divergent spellings of type queries and type casts):

  • nullopt_t is the empty state for std::optional
  • nullptr_t is the empty state for raw/smart pointers
  • More generally, the default-constructed state T() is the empty state for all Pointer-like things including iterators, and this is already the way the Lifetime profile handles it: a Pointer is any type that can be dereferenced, and a default-constructed Pointer is considered its null/empty value (including that an STL iterator is treated identically to a default-constructed (null) raw or smart pointer) and you can already see this in cppfront’s cpp2util.h null test (currently at line 298). [Edited to add: Note that it turns out the Standard doesn’t make this usefully testable for STL iterators, because it says that a default-constructed STL iterator can only be reliably compared to another default-constructed one. So while the default-constructed state is indeed an “empty” state for the iterator, as far as I know there is no way to portably test whether a given STL iterator object is actually in that state. Equality testing against a default-constructed iterator may work or it may not.]
  • monostate (and arguably valueless_by_exception) is the empty state for std::variant
  • !has_value is the empty state for std::any
  • !is_ready (which has a longer spelling in today’s standard library) is the empty state for std::*future

And so we have an opportunity to unify these too, which goes beyond what I showed last year and in my previous revision of paper P2392.

But wait, on top of all that, the language itself has already had a way to spell “no type” since the 1970s: void. And even though void is not a regular type (it doesn’t work as a type in some places in the C++ type system) it works in enough of the places we need to implement is void as the generic spelling of “is empty.”

A possible convergence: is void

So today I implemented is void as a generic “empty state” test in Cpp2 syntax in cppfront. I also checked in the following Cpp2-syntax test case, which now works as self-documented — and I couldn’t resist the nod to William Tyndale:

main: () -> int = {
    p: std::unique_ptr<int> = ();
    i: std::vector<int>::iterator = ();  // see "edited to add" note above
    v: std::variant<std::monostate, int, std::string> = ();
    a: std::any = ();
    o: std::optional<std::string> = ();

    std::cout << "\nAll these cases satisfy \"VOYDE AND EMPTIE\"\n";

    test_generic(p);
    test_generic(i);
    test_generic(v);
    test_generic(a);
    test_generic(o);
}

test_generic: ( x: _ ) = {
    std::cout
        << "\n" << typeid(x).name() << "\n    ..."
        << inspect x -> std::string {
            is void = " VOYDE AND EMPTIE";
            is _    = " no match";
           }
        << "\n";
}

Note that this generic function would be impossible to write without some kind of is void unification to eliminate all of today’s non-generic divergent “empty state” queries.

Here’s the result on my machine in Ubuntu using GCC and libstdc++… I’m glad to show GCC here after having my machine’s WSL 2 subsystem quit on me on-stage so that I couldn’t show the GCC and Clang live demos in the CppCon 2022 talk (sigh!):

Implementing it ensured the implementation worked, including that it exposed where an if constexpr is needed for std::variant‘s is void test (see cpp2util.h, currently lines 610-614; note that empty is an alias for void). Once I got it working in cppfront (prototypes matter! they help us debug our proposals) I added it to the next draft of my ISO C++ proposal paper P2392 for is/as/inspect in today’s Cpp1 syntax, including the suggested implementation.

Thanks, Drew!