# From C++ — `std::variant` without the visitor, and five smaller mappings

Every concept on this page is one you have. `std::variant`, `std::visit`, `std::optional`, `auto`,
raw string literals. What is worth your time is the mapping, plus the four places where Java's
version behaves differently enough to change a design.

The first section is most of the lesson. The rest are one-screen mappings.

## The closed variant set

`corpus/file-system` has a tree whose nodes are files or directories, and there is no third kind.
In C++ that is a variant with two alternatives.

**C++**

```cpp
struct Node;                       // the variant, forward declared for the recursion

struct FileNode      { std::string content; };
struct DirectoryNode { std::map<std::string, std::unique_ptr<Node>> children; };

struct Node { std::variant<FileNode, DirectoryNode> value; };

EntryKind kindOf(const Node& node) {
    return std::visit(overloaded{
        [](const FileNode&)      { return EntryKind::File; },
        [](const DirectoryNode&) { return EntryKind::Directory; },
    }, node.value);
}
```

**Java** — `corpus/file-system/reference/src/Node.java` and `Nodes.java`, verbatim apart from the
comments:

```java
sealed interface Node permits FileNode, DirectoryNode {}

final class FileNode implements Node { … }
final class DirectoryNode implements Node { … }

static EntryKind kindOf(Node node) {
    return switch (node) {
        case FileNode file -> EntryKind.FILE;
        case DirectoryNode dir -> EntryKind.DIRECTORY;
    };
}
```

Line up what each half is doing. The `permits` clause is the alternative list, and in both languages
it is the one place the set is written down. `std::visit` refuses a visitor that misses an
alternative; the `switch` refuses a case list that misses a permitted type. Same guarantee, from
the same declaration.

Three things went away. The `overloaded` helper, which is boilerplate you write once per project
and explain to every new hire. The visitor object itself. And `std::get_if` with its pointer
result, because `case FileNode file` binds a typed `file` with no cast written anywhere.

One thing that also went away, and it is worth noticing: the forward-declared `struct Node`
wrapper. A `std::variant` needs its alternatives to be complete types, so a recursive variant needs
an indirection you insert by hand. A Java interface reference is already an indirection, so
`DirectoryNode` holding a `TreeMap<String, Node>` needs nothing.

### The check, and the day it fires

Curveball 01 of `file-system` asks for shortcuts: a name in the tree that points at another path.
That is a third kind of node where the problem said there were two.

Take the real reference solution, add `SymlinkNode` to the `permits` clause, add the new file, and
change nothing else. One command, real output:

```
Nodes.java:14: error: the switch expression does not cover all possible input values
        return switch (node) {
               ^
1 error
```

One error, in the one file whose job is to know. `javac` names the line, and the fix is the case it
demanded:

```java
            case SymlinkNode link -> throw new IllegalStateException(
                    "PathResolver must never return an unresolved shortcut");
```

Read that arm carefully, because it is the part people find strange. The case cannot happen —
resolution follows shortcuts, so an unresolved one never reaches here. The compiler made you write
a decision for an impossible state anyway, and the decision is recorded rather than assumed.

**What the whole curveball cost, measured.** `curveballs/01-shortcuts/budget.json` records
`reference_diff = 28`, and `reference-patch/PATCH.md` breaks it down: `TreeResolver.java` 16 lines,
`InMemoryFileSystem.java` 8, `Node.java` 2, `Nodes.java` 2.

Four of the 28 belong to the sealed hierarchy. `Node.java`'s two are one word added to a `permits`
list, charged as a removal and a re-addition because a line-level diff sees the line rewritten.
`Nodes.java`'s two are the forced case.

The other 24 are the requirement itself. Seven of the eight operations changed by zero lines,
because they were written in terms of `PathResolver` rather than a map they walked themselves. So
the sealed type is not what made this change cheap. It is what made the change **complete** — the
compiler produced the list of places that had a decision to make, and the list had one entry.

### The three ways to lose the check

**A `default` arm.** Same reference, same edit, but `kindOf` written with two cases and a default:

```java
        return switch (node) {
            case FileNode file -> EntryKind.FILE;
            default -> EntryKind.DIRECTORY;
        };
```

Adding the variant now compiles clean. Real output from running it:

```
kindOf(new SymlinkNode("/logs/app.log")) = DIRECTORY
```

A shortcut is reported as a directory. Nothing threw, nothing warned, and `kindOf` is what a caller
uses to decide whether to call `read` or `ls`.

**An `instanceof` chain.** This is the version a competent engineer writes before meeting sealed
types, and it is what J5's `Appender` hierarchy would have forced:

```java
        if (node instanceof FileNode) {
            return EntryKind.FILE;
        }
        return EntryKind.DIRECTORY;
```

Also compiles clean after the edit, also answers `DIRECTORY`. Measured the same way, same output.

**A visitor with a generic parameter.** The C++ half has the identical hole:

```cpp
std::visit([](auto&&) { return EntryKind::Directory; }, node.value);
```

So this is not a Java lesson about C++ being careless. It is the same trade in both languages: a
catch-all arm buys you silence today and costs you the compile error you wanted.

The rule that follows is short. **A `switch` over a sealed type never has a `default`.** If you
cannot answer a case, throw in it, the way `Nodes.kindOf` does.

### Guards, deconstruction, and the two orderings that matter

The switch does more than one test per case. `worked/src/Nodes.java` has five answers over three
variants:

```java
        return switch (node) {
            case FileNode file when file.content().isEmpty() -> "an empty file";
            case FileNode file -> "a file of " + file.content().length() + " characters";
            case DirectoryNode dir when dir.childCount() == 0 -> "an empty directory";
            case DirectoryNode dir -> "a directory of " + dir.childCount() + " entries";
            case SymlinkNode(String target) -> "a shortcut to " + target;
        };
```

`when` is a guard on the case, so a condition about a variant lives beside the variant instead of
in a nested `if`. `case SymlinkNode(String target)` is a record deconstruction pattern: it matches
the type and binds the component in one go, with no accessor call.

Two ordering rules, both enforced. A guarded case must come before the unguarded case for the same
type, because the unguarded one covers everything:

```
T9.java:5: error: this case label is dominated by a preceding case label
            case FileNode f -> "file";
                 ^
1 error
```

And a guard never counts towards completeness. Drop the two unguarded arms and the switch is
incomplete again, because `javac` will not reason about whether `isEmpty()` always holds:

```
TA.java:3: error: the switch expression does not cover all possible input values
        return switch (node) {
               ^
1 error
```

### `case null`, which C++ has no need for

A variant always holds something. A Java reference does not, so a pattern switch has to say what
it does about null. The default behaviour is to throw:

```java
Nodes.describe(null)
```

```
java.lang.NullPointerException
```

That is usually what you want, and it is why `Nodes.kindOf` has no null case. When null is a
legitimate input, say so, and the switch stops throwing. `worked/src/MiniFileSystem.write` is that
case, because `parent.child(leaf)` returns null for a name that is not there yet:

```java
        switch (parent.child(leaf)) {
            case null -> parent.addChild(leaf, new FileNode(content));
            case FileNode file -> file.write(content);
            case DirectoryNode dir -> throw new IsADirectoryException(path);
            case SymlinkNode link -> throw new IllegalStateException(…);
        }
```

Those four arms are curveball 01's real fix to `write`, and `PATCH.md` charges 4 of its 8 lines in
that file to it.

### When a sealed hierarchy is the wrong answer

Two cases, and both come up in the corpus.

**The variants carry no data.** Then it is an `enum`, and an enum switch is checked the same way.
`EntryKind` in this very problem is an enum for exactly that reason. J10 is enums.

**You want other people to add variants.** A plugin point, a policy interface, a strategy —
anything where a second team's class is meant to slot in. `Appender` in `corpus/logger` is an
ordinary interface for that reason, and sealing it would break the curveball that adds a rolling
destination. J5's threshold list ends here on purpose: the question is whether you know every
implementor and want to keep knowing.

`corpus/trip-state-machine/contract/TripEvent.java` is the other strong case in the corpus, and its
javadoc makes the argument in one paragraph worth stealing for an interview. Its events carry data
that differs per occurrence: `Matched` names a driver, `RiderCancelled` names a reason. An `enum
Kind` plus a nullable `driverId` and a nullable `reason` would permit a `TRIP_STARTED` carrying a
driver id. With five records behind a sealed interface, that state is unconstructable, so nothing
validates it.

## `record`

`SymlinkNode` in the corpus is a final class with one final field, one constructor and one
accessor. Eight non-blank lines. As a record it is one:

```java
record SymlinkNode(String target) implements Node {}
```

That header generates five things. A canonical constructor taking the components in order. An
accessor per component, named after it. `equals` comparing every component, and a `hashCode`
consistent with that `equals`. And a `toString` naming the class and each component. Block 2 of
`worked/` prints what arrived:

```
--- 2. what the record header generated
  Listing.class.isRecord()       : true
  components                     : String path, EntryKind kind, int childCount
  toString                       : Listing[path=/logs, kind=DIRECTORY, childCount=3]
  accessor one.childCount()      : 3
  one == two                     : false
  one.equals(two)                : true
  hashCodes agree                : true
  List.of(one).contains(two)     : true
```

`one == two` is `false` and `one.equals(two)` is `true`, for two separately constructed values with
identical components. That is J6's trap, and a record is the one declaration in Java that cannot
fall into it. `List.of(one).contains(two)` returning `true` is the same fact from the caller's side.

### Say what a record is not

**It is not a struct.** Its components are final, and there is no way to add a non-static field:

```
T6.java:2: error: cannot assign a value to final variable childCount
    void bump() { childCount = childCount + 1; }
                  ^
1 error
```

```
T7.java:2: error: field declaration must be static
    private int hits;
                ^
  (consider replacing field with record component)
1 error
```

The second message is the useful one. `javac` tells you the only place state may go in a record: the
header.

**It is not a base class and it has no base class.** A record extends `java.lang.Record` already, so
the grammar has no room for another:

```
T8.java:2: error: '{' expected
record R(String s) extends Base {}
                  ^
1 error
```

It can implement any number of interfaces, which is what makes the sealed hierarchy work.

**Final components are not deep immutability.** A record holding a `List` hands the same list to
every caller through the generated accessor, and there is no copy. That is A7's subject, and it is
why `DirectoryNode` in `worked/src` is a class rather than a record with a `TreeMap` component.

### The compact constructor

This is where validation and normalisation go, and it is the piece with no C++ counterpart. The
form drops the parameter list and the assignments:

```java
record SymlinkNode(String target) implements Node {

    SymlinkNode {
        Objects.requireNonNull(target, "target");
        if (!target.startsWith("/")) {
            throw new IllegalArgumentException("a shortcut target must be absolute: " + target);
        }
        if (target.length() > 1 && target.endsWith("/")) {
            target = target.substring(0, target.length() - 1);
        }
    }
}
```

The generated constructor assigns the parameters to the fields **after** this body runs. So `target`
is still an ordinary local here, and reassigning it changes what the field ends up holding. That
last `if` is the only legal place in a record to normalise a component.

Block 3 of `worked/` runs both jobs:

```
--- 3. the compact constructor, doing the two jobs it exists for
  "/logs/" normalised to         : /logs
  equal to the version without   : true
  a negative child count         : java.lang.IllegalArgumentException: childCount must not be negative: -1
  a relative path                : java.lang.IllegalArgumentException: path must be absolute: logs
  new SymlinkNode("/logs/")      : SymlinkNode[target=/logs]
  equal to the version without   : true
  a null target                  : java.lang.NullPointerException: target
```

Normalising once, here, is what makes `equals` mean what a reader expects. Two spellings of one
path are one value. Skip it and every comparison in the program has to know that `/logs/` and
`/logs` are the same place.

J4 is where the general construction rules live: `final` fields, no initialiser lists, static
factories. A record is those rules with the typing done for you. The compact constructor is J4's
"validate before the object exists", moved to the one place every caller passes through.
`corpus/trip-state-machine/contract/Transition.java` is four `requireNonNull` calls in a compact
constructor and nothing else.

The corpus has **88 record declarations** and **four** sealed hierarchies: `Node` in `file-system`,
`TripEvent` in `trip-state-machine`, `SquareEffect` in `snake-and-ladder`, and `Segment` in
`middleware-router`. Records are ordinary here. Sealing is a decision you make four times in twenty
problems.

## `var`

`auto` and `var` are both local type inference, and `auto` goes to more places. That asymmetry is
the whole delta.

`var` is legal for a local variable with an initialiser, a `for` loop variable, a for-each variable,
a try-with-resources resource, and a lambda parameter. Nowhere else, and the message is the same
each time:

```
T3.java:2: error: 'var' is not allowed here
    var field = 1;
    ^
T3.java:3: error: 'var' is not allowed here
    static int f(var x) { return x; }
                 ^
T3.java:4: error: 'var' is not allowed here
    static var g() { return 1; }
           ^
T3.java:8: error: 'var' is not allowed as an element type of an array
        var[] c = new int[3];
        ^
4 errors
```

No field, no method parameter, no return type, no array element type, and no catch parameter. In
C++ a deduced return type and an `auto` parameter are both ordinary; in Java a signature always
states its types. That is a language decision rather than an omission: a signature is the part other
files read.

Two more refusals, and both are the same rule from two sides:

```
T4.java:3: error: cannot infer type for local variable a
        var a = null;
            ^
  (variable initializer is 'null')
```

```
T5.java:3: error: cannot infer type for local variable b
        var b;
            ^
  (cannot use 'var' on variable without initializer)
```

**The threshold.** Use `var` when the right-hand side already names the type, and write the type out
when it does not. `var rows = new ArrayList<Listing>()` says `ArrayList<Listing>` twice, so one of
the two is noise. `var found = fs.find("/logs/latest")` says nothing about what was found, and the
reader has to open another file. Block 5 of `worked/` is that pair:

```
--- 5. var, where the right-hand side already names the type
  var rows = new ArrayList<Listing>() : ArrayList, holding 1
  var row  = new Listing(..)           : Listing
  Optional<Node> found = fs.find(..)   : Optional is all 'var found' would have told a reader
```

The corpus agrees with that threshold by using `var` three times across 638 Java files. All three
sit in `corpus/splitwise/curveballs/01-weighted-shares/tests/`, and all three are assigned from a
call whose name carries the type. `var` is not a style you owe anyone. In a twelve-minute
skeleton sprint it saves you retyping `Map<String, List<Transition>>`, and that is the case for it.

## `Optional`

`std::optional<T>` maps to `Optional<T>` for the one thing that matters: a return value that may
have nothing in it, with the emptiness in the type instead of in a comment. `corpus/lru-cache`'s
`get` returns `Optional<Object>`, and `corpus/library`'s three repositories return
`Optional<Title>`, `Optional<Loan>` and `Optional<Member>`. There are 64 occurrences across the
corpus outside comments. Not one of them is a method parameter, and not one is an ordinary field.

Three differences, in order of how much trouble they cause.

**An `Optional` is an object on the heap, and the reference can itself be null.**
`std::optional<T>` is a `T` plus a flag, stored inline, and it cannot not-exist. Java's cannot make
that promise, and block 6 of `worked/` shows the failure:

```
  an Optional reference is itself nullable:
    java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because "Main.cached" is null
```

Three states where C++ has two: null, empty, present. That third state is the argument against
declaring one as a field.

**`get()` is a null dereference wearing a different name.** No check, no warning, and the message
names nothing:

```
  get() on the empty one         : java.util.NoSuchElementException: No value present
```

`std::optional::operator*` has the same hazard and you already route around it. The Java habit is to
use the three methods that force the decision, all shown in block 6:

```
  find(/logs/latest)             : Optional[SymlinkNode[target=/logs/app.log]]
  find(/var/missing.log)         : Optional.empty
  map then orElse                : nothing is there
  orElseThrow, which get(path) is: NoSuchPathException: /var/missing.log
```

`orElseThrow(supplier)` builds the exception only when there is nothing there, and the exception can
name the path. That one line is the whole body of `MiniFileSystem.get`:

```java
    Node get(String path) {
        return find(path).orElseThrow(() -> new NoSuchPathException(path));
    }
```

`map` is where a chain of "if it is there, then" collapses. `orElse` supplies the fallback. If your
code calls `isPresent()` and then `get()`, you have written an `if (p != null)` with more ceremony.

**It is a return type, not a field type and not a parameter type.** This is convention rather than
compiler enforcement, and the reasons are concrete. `Optional` is not `Serializable`. A field of
that type has the three states above. A parameter of that type makes every caller wrap a value it
already has, where an overload or a documented null costs nothing.

The corpus has two `Optional`s in a field position, both in `corpus/tic-tac-toe/contract/`, and both
are instructive rather than contradictions. `MoveResult` is a record returned from `play()` with an
`Optional<Mark> winner` component. `GameState` is a record returned from `state()` with the same
component plus a `List<Optional<Mark>> cells`.

Look at what each compact constructor does first. `MoveResult` calls
`Objects.requireNonNull(winner, "winner")`, which removes the third state, and then enforces that
`winner` is present exactly when the status is `WON`. `GameState` does the same in four
`requireNonNull` calls. A component of a returned value is still part of a return type, and both of
them pay for the privilege with a null check.

## Text blocks

`"""` is Java's raw string literal, and it does the two jobs you use `R"(...)"` for: no escaping,
and real line breaks. It adds one thing C++ does not have, which is incidental indentation
stripping. The common leading whitespace across all lines, including the closing delimiter's line,
is removed.

That makes it useful for one thing in this project: a multi-line expected value in a test, written
at the indentation of the code around it. Block 7 of `worked/`:

```java
        String expected = """
                /logs/app.log   FILE      0
                /logs/empty.log FILE      0
                /logs/latest    FILE      0
                """;
```

```
--- 7. a text block holding the expected listing
  actual.equals(expected)        : true
```

Two mechanical points. The opening `"""` must be followed by a line break, and the error if it is
not names the problem:

```
TC.java:2: error: illegal text block open delimiter sequence, missing line terminator
    static final String S = """/a
                               ^
```

And the closing delimiter's own line decides whether there is a trailing newline. Put `"""` on its
own line and the string ends with `\n`; put it at the end of the last content line and it does not.

That is the whole feature. It is a convenience for test fixtures and prompt strings, and no design
decision turns on it.

## The delta table

| C++ | Java | Watch for |
|---|---|---|
| `std::variant<A, B>` | `sealed interface T permits A, B` | the seal is on the interface, not at the use site |
| the alternative list | the `permits` clause | both are the one place the set is written |
| `std::visit` with a full visitor | `switch` over patterns, no `default` | same completeness guarantee |
| `overloaded{...}` helper | nothing to write | the case list is the visitor |
| `std::get_if<A>(&v)` | `case A a ->`, or `if (v instanceof A a)` | the pattern binds; no cast appears |
| `std::visit` with `auto&&` | a `default` arm | both silence the check |
| `std::holds_alternative<A>` | `instanceof A` | no index, no `bad_variant_access` |
| variant stored inline, size of largest | a reference, always heap | no value semantics, no copy cost |
| the same type twice, by index | impossible; a type is its own name | |
| recursive variant needs an indirection | a reference is one already | no wrapper struct |
| `struct` with public members | `record`, and the components are final | not a struct; no non-static field allowed |
| a constructor body that validates | the compact constructor | the only place a component can be reassigned |
| `operator==` you wrote | generated `equals` over all components | J6's trap cannot happen here |
| `auto` on locals, parameters, return types | `var` on locals only | a signature always states its types |
| `auto x = ...;` as a class member | not allowed either | `var field` is `'var' is not allowed here` |
| `std::optional<T>` inline, cannot be absent | `Optional<T>` on the heap, can be null | three states, not two |
| `*opt` with no check | `opt.get()` with no check | `NoSuchElementException: No value present` |
| `opt.value_or(x)` | `orElse(x)`, or `orElseGet(supplier)` | `orElse` evaluates its argument always |
| `opt.value()` throwing `bad_optional_access` | `orElseThrow(() -> yourException)` | yours can name the path |
| `std::optional` as a member, common | an `Optional` field, avoided | not `Serializable`, and the third state |
| `R"(raw)"` | `"""` text block | must break the line after the opening delimiter |
| no indentation handling | incidental indentation stripped | the closing delimiter's line sets the margin |

## Under a clock

The grading angle in one line, because it changes what you type in the first three minutes of a
drill. **A `record` beats a hand-written value class every time.** It is one line against eight, and
it cannot get `equals` wrong.

`sealed` plus a switch is the same argument at design scale. When the interviewer says "now add a
symlink," you add one word to `permits` and compile. The list of places that need a decision comes
back from the compiler.
