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).
Problem
mx::apiround-trips optional-with-default fields by pairing a value with a parallel "specified" bool:ClefData::line+isLineSpecified(api: round-trip injects implied default elements:<type><line><voice><staff>#228)ClefData::octaveChange+isOctaveChangeSpecified(api: round-trip omits fields whose value is 0 (clef-octave-change, transpose diatonic, stem @default-y) #229)TransposeData::diatonic+isDiatonicSpecified(api: round-trip omits fields whose value is 0 (clef-octave-change, transpose diatonic, stem @default-y) #229)TimeSignatureData::isImplicitThe 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-
trueflags, 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::clefsis a vector; the object's existence already means "emit this," yet a flag can suppress it) and more defensible for static-per-measure data liketimeSignature. Breaking: it changes publicScoreDatafield types.Options
Solution A —
std::optional<T>— intrinsic presence, but.value()throws andoperator*is UB on empty; safe defaulting needsvalue_ordiscipline.Solution B —
std::variant<std::monostate, T>— Rust-Option-like withstd::visitexhaustiveness, but heavy for one payload andstd::getstill throws. (Right tool for multi-case choices likeAttributesChoice, not a lone int.)Solution C — custom fused type (recommended) — value + presence in one object; reads never throw, writes set both atomically:
Solution C is the only one giving both intrinsic presence and safe default reads.
Naming
Optionalis a working name only — TBD, since it clashes withstd::optionaland 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 reconcileisImplicit. Preserve round-trip fidelity; update accessors andMXAPI_EQUALS.References
#229, #228 (introduced these bools); #99 (typed-wrapper philosophy).