Skip to content

Allow a trivially copyable type to opt out of the byte copy - #196

Merged
TrentHouliston merged 1 commit into
mainfrom
houliston/serialise-trivially-optout
Aug 13, 2026
Merged

Allow a trivially copyable type to opt out of the byte copy#196
TrentHouliston merged 1 commit into
mainfrom
houliston/serialise-trivially-optout

Conversation

@TrentHouliston

@TrentHouliston TrentHouliston commented Aug 13, 2026

Copy link
Copy Markdown
Member

🤖 Filed on behalf of @TrentHouliston by an agent.

The problem

Serialise<T, Check>'s specialisations are selected by conditions that are assumed to be mutually exclusive. A type that satisfies two of them doesn't prefer one — it makes Serialise<T> ambiguous. That leaves the trivially copyable specialisation with no way to opt out of.

The motivating case is a code generator whose message classes are plain structs of scalars. They are trivially copyable, but they carry their own wire format and their own type identity (a hash over the schema's fully qualified name, shared with peers on the other end of the wire). The byte copy bypasses both: it would send the object representation, and hash() would identify the type by its demangled C++ name.

The generator wants one partial specialisation covering everything it emits:

template <typename T>
struct Serialise<T, std::enable_if_t<is_generated<T>::value, T>> { ... };

which ties with

template <typename T>
struct Serialise<T, std::enable_if_t<std::is_trivially_copyable<T>::value, T>> { ... };

Neither is more specialised than the other, so it's ill-formed:

error: ambiguous template instantiation for 'struct NUClear::util::serialise::Serialise<Encoded>'
note: candidates are: 'template<class T> struct Serialise<T, enable_if_t<is_trivially_copyable<T>::value, T> >'
                      'template<class T> struct Serialise<T, enable_if_t<is_generated<T>::value, T> >'

A full explicit specialisation does win outright, so the workaround is one specialisation per type — which defeats the point when the set of types is whatever the generator emitted.

The change

Route the condition through a trait that can be specialised:

template <typename T, typename = void>
struct serialise_trivially : std::is_trivially_copyable<T> {};

template <typename T>
struct Serialise<T, std::enable_if_t<serialise_trivially<T>::value, T>> { ... };

The second parameter is an SFINAE slot, so a family of types is opted out with a single partial specialisation rather than one per type. The container specialisation goes through the same trait, so a container of opted-out elements stops claiming the byte copy too.

The default is std::is_trivially_copyable, so nothing changes for any type that does not opt out.

Tests

tests/tests/util/serialise/serialise_trivially.cpp is new. It sets up exactly the shape above — a trivially copyable type, a trait marking the family, a partial specialisation of Serialise — and asserts the type's own wire format and hash are used, plus that a type which has not opted out still gets the byte copy.

It does not compile before this change, and passes after it.

The existing serialise.cpp tests are unchanged and still pass (60 assertions, 6 cases).

Note on local verification: this repo's tests need CMake ≥ 3.24 for $<LINK_LIBRARY:WHOLE_ARCHIVE,...> and the container I built in has 3.23.2, so I compiled and ran both test files directly against the header rather than through CTest. CI covers the CMake path.

🤖 Generated with Claude Code

The specialisations of Serialise are selected by conditions assumed to be mutually
exclusive, so a type that satisfies two of them makes Serialise<T> ambiguous rather
than preferring one. That makes the trivially copyable specialisation unreachable to
opt out of: a code generator emitting plain structs of scalars cannot supply the
encoding and type identity those messages carry, because its partial specialisation
ties with the byte copy and neither is more specialised than the other.

A full specialisation would win outright, but that is one specialisation per type
rather than one covering everything a generator emits.

Route the condition through a serialise_trivially trait so it can be specialised to
false. The second parameter is an SFINAE slot, so a family of types can be opted out
with a single partial specialisation. The default is std::is_trivially_copyable, so
behaviour is unchanged for every type that does not opt out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TrentHouliston
TrentHouliston force-pushed the houliston/serialise-trivially-optout branch from 29d6894 to 4253210 Compare August 13, 2026 00:13
@TrentHouliston
TrentHouliston merged commit 667aeb7 into main Aug 13, 2026
15 checks passed
@TrentHouliston
TrentHouliston deleted the houliston/serialise-trivially-optout branch August 13, 2026 02:15
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.

1 participant