Skip to content

fix(cplusplus): forward-declare recursive unions instead of self-referential aliases#3046

Open
schani wants to merge 5 commits into
masterfrom
agent/fix-issue-273
Open

fix(cplusplus): forward-declare recursive unions instead of self-referential aliases#3046
schani wants to merge 5 commits into
masterfrom
agent/fix-issue-273

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

The bug

Issue #273 tracks "Detect and properly handle recursive types" across
quicktype's backends. The concrete, still-reproducing report (2021 comment,
confirmed on current master) is in the C++ backend: a recursive union that
refers back to itself through a container (e.g. std::vector) was emitted
as a plain type alias:

using RecursiveThing = std::variant<std::vector<RecursiveThing>, std::string>;

This does not compile with g++ -std=c++17:

error: 'RecursiveThing' was not declared in this scope

because a using alias's name is not in scope on its own right-hand side —
unlike a struct/class, whose name is visible immediately after its
opening brace.

Root cause

CPlusPlusRenderer.isImplicitCycleBreaker treated array/map container
types as unconditional cycle breakers, so a union reachable only through a
container was never flagged as needing forward declaration/heap indirection
the way recursive classes already are. emitUnionTypedefs then always
emitted named unions as a bare using alias, which is only valid when the
union's own name doesn't appear inside its own definition.

The fix

  • A container (array/map) is now only treated as an implicit cycle
    breaker when none of its children are (or contain) a union — since
    breaking the cycle for a union alias doesn't actually work.
  • Added isRecursiveUnion to detect a named union that reaches itself
    through its members.
  • emitUnionTypedefs now emits such recursive unions as a forward-declared
    struct X : std::variant<...> wrapper (with inherited constructors/
    assignment) instead of an invalid self-referential using alias, mirroring
    how recursive classes are already handled.
  • Added from_json/to_json free-function forwarding for the wrapper struct
    so (de)serialization still goes through the underlying variant's
    adl_serializer.
  • canBreakCycles now also recognizes UnionType (previously only
    ClassType) as able to break declaration cycles.
  • Overrode CPlusPlusRenderer.emitIncludes's <optional> inclusion to also
    cover cases (haveNamedUnions && !boost) that only arise via wrapper
    structs.

Test coverage

test/inputs/schema/recursive-union-flattening.schema already exercised
this exact bug shape and was listed in CPlusPlusLanguage.skipSchema in
test/languages.ts with the comment "Recursive top-level unions produce
aliases that can refer to later aliases." That schema is now un-skipped and
serves as the regression test — it was verified to fail (bad/non-compiling
output) before the fix and to pass after.

Verification performed locally

  • npm run build passes.
  • Manually reproduced the original bug report's repro command
    (--lang c++ --code-format with-struct --namespace schema --src-lang schema --no-boost) against a minimal recursive schema; confirmed the invalid
    using alias before the fix, and a compiling forward-declared struct
    wrapper after. Compiled the generated header with g++ -std=c++17 and
    additionally wrote a small round-trip driver confirming
    parse→serialize produces the original JSON.
  • QUICKTEST=true FIXTURE=schema-cplusplus script/test: all 70/70 samples
    pass, including the newly un-skipped recursive-union-flattening.schema.
  • QUICKTEST=true FIXTURE=cplusplus script/test: all samples pass except
    two pre-existing, unrelated failures caused by Boost headers not being
    installed in this sandbox (boost: true renderer-option cases), which are
    not touched by this change.
  • npm run test:unit: 170/170 tests pass.
  • CI will additionally validate this across the full (non-QUICKTEST) fixture
    matrix.

Fixes #273

🤖 Generated with Claude Code

schani and others added 2 commits July 20, 2026 18:36
…rential aliases (#273)

Recursive unions reachable through a container (e.g. a union that contains
itself via std::vector) were emitted as a plain `using X = std::variant<...,
X, ...>;` alias, which g++ rejects because X is not in scope on its own
right-hand side. Such unions are now emitted as a forward-declared struct
that inherits from the variant, matching how recursive classes are already
handled.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Generated-output differences

5 files differ — 4 modified, 1 new, 0 deleted
495 changed lines — +476 / −19

Open the generated-output report →

@schani

schani commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

This is odd. It includes <optional> in many cases where it's not needed. And then in some cases where it changes something else it doesn't include <optional>.

@github-actions

Copy link
Copy Markdown

Generated-output differences

5 files differ — 4 modified, 1 new, 0 deleted
495 changed lines — +476 / −19

Open the generated-output report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect and properly handle recursive types

1 participant