Skip to content

api: replace value + isSpecified bool pairs with an explicit optional value type #249

Description

@webern

Problem

mx::api round-trips optional-with-default fields by pairing a value with a parallel "specified" bool:

The value and the bool are two sources of truth kept in sync by hand. Forget the bool and the field silently fails to round-trip (or, for default-true flags, appears unbidden). It only bites the authoring path — the reader always sets the bool — so it is a pure public-API tax. It is most awkward for event-like data (StaffData::clefs is a vector; the object's existence already means "emit this," yet a flag can suppress it) and more defensible for static-per-measure data like timeSignature. Breaking: it changes public ScoreData field types.

Options

Solution A — std::optional<T> — intrinsic presence, but .value() throws and operator* is UB on empty; safe defaulting needs value_or discipline.

Solution B — std::variant<std::monostate, T> — Rust-Option-like with std::visit exhaustiveness, but heavy for one payload and std::get still throws. (Right tool for multi-case choices like AttributesChoice, not a lone int.)

Solution C — custom fused type (recommended) — value + presence in one object; reads never throw, writes set both atomically:

template <typename T>
class Optional // name TBD: clashes with std::optional
{
    T myValue{};
    bool myIsSpecified = false;

  public:
    bool isSpecified() const { return myIsSpecified; } // drives the writer
    T get() const { return myValue; }                  // always safe; default when unset
    void set(T v) { myValue = v; myIsSpecified = true; }
    void clear() { myValue = T{}; myIsSpecified = false; }
};

Solution C is the only one giving both intrinsic presence and safe default reads.

Naming

Optional is a working name only — TBD, since it clashes with std::optional and behaves differently (defaults instead of throwing). The final name should signal the defaulting behavior (Defaulted / Specifiable / Tracked).

Scope

Migrate #229 fields first (octaveChange, diatonic), then #228 (isLineSpecified, isDurationNameSpecified), then reconcile isImplicit. Preserve round-trip fidelity; update accessors and MXAPI_EQUALS.

References

#229, #228 (introduced these bools); #99 (typed-wrapper philosophy).

Metadata

Metadata

Assignees

No one assigned

    Labels

    aiIssues opened by, or through, a coding agent.apiAffects the mx::api layerbreakingfixes or implementation that require breaking changesfeaturenew feature requestimplAffects the mx::impl layer

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions