Reorder C and C++ Struct Fields Safely with clang-reorder-fields
You will finish with a previewable way to reorder the fields of a C or C++ struct or class, while letting clang-reorder-fields update supported initialisers. The examples use the installed LLVM 20 tool, version 20.1.8, from package clang-tools-20. Allow about fifteen minutes, plus time to review the resulting diff. You need a shell, a source file, and a clean or otherwise recoverable working tree.
The route
Jump straight to the step you need, or tick off Done means at the end.
This tool changes source text. It does not optimise a binary in place, and changing field order can change object layout and program behaviour. Keep the preview output, make a backup or use version control, and compile and test the result before accepting it.
1. Check the installed command
Start with read-only checks. No elevated privileges are needed:
$ command -v clang-reorder-fields-20
/usr/bin/clang-reorder-fields-20
$ clang-reorder-fields-20 --version
Ubuntu LLVM version 20.1.8
Optimized build.
$ dpkg-query -W -f='${Package} ${Version}\n' clang-tools-20
clang-tools-20 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139
The installed manual describes the command as clang-reorder-fields [options] <source0> [... <sourceN>]. The packaged executable has the versioned name, so use clang-reorder-fields-20 in scripts unless you have deliberately provided another command name.
2. Inspect the record and choose an exact order
--record-name identifies the struct or class. --fields-order takes a comma-separated list containing every field, once, in its desired order. Names must match the definition. For a namespaced C++ type, use its fully qualified name, such as ::network::Packet.
Suppose packet.cpp contains this record:
struct Packet {
int payload;
char kind;
short flags;
};
To request kind, then flags, then payload, pass one comma-separated value:
$ clang-reorder-fields-20 \
--record-name=Packet \
--fields-order=kind,flags,payload \
packet.cpp
Checkpoint: the command should print the rewritten source to standard output and leave packet.cpp unchanged. Without -i, redirecting that output to a new file is a safe way to inspect it:
$ clang-reorder-fields-20 --record-name=Packet \
--fields-order=kind,flags,payload packet.cpp > packet.cpp.new
$ diff -u packet.cpp packet.cpp.new
--- packet.cpp
+++ packet.cpp.new
@@
+ char kind;
+ short flags;
int payload;
- char kind;
- short flags;
}
The program may print compilation-database diagnostics when no database is available, then continue with no compiler flags. That is not the same as a successful semantic check. If the source needs project-specific include paths or language settings, supply them explicitly or use a compilation database in the next step.
3. Supply the same compiler context as the project
Use -p with the directory containing compile_commands.json. This keeps the tool's parsing context aligned with the project:
$ clang-reorder-fields-20 -p build \
--record-name=Packet \
--fields-order=kind,flags,payload \
src/packet.cpp > src/packet.cpp.new
For a small standalone C++20 example, an extra compiler argument can select the language standard:
$ clang-reorder-fields-20 --extra-arg=-std=c++20 \
--record-name=Packet \
--fields-order=kind,flags,payload packet.cpp > packet.cpp.new
--extra-arg appends an argument and --extra-arg-before prepends one. Use them for the minimum context needed to parse the file. Do not copy a whole build command into these options without checking how the shell will split it.
4. Review initialisers and layout changes
clang-reorder-fields can update aggregate initialisers and, for C++, constructor initialiser lists. It also handles C++20 designated initialisers when the source is parsed as C++20. That is useful, but it does not make the change risk-free:
struct Packet {
int payload;
char kind;
short flags;
};
Packet packet = { 42, 'D', 3 };
After reordering, the positional aggregate values must follow the new declaration order. Inspect every changed initialiser and every caller that depends on the layout. A C++ class can also have dependencies between member initialisers. Members are initialised in declaration order, not the order written in the constructor. The tool can warn when a reordered member is used before it has been initialised, but the warning does not repair the logic.
Do not assume that a smaller object is guaranteed. Field order can reduce padding on one target and produce a different result on another. Check representative targets with a test or a deliberately instrumented build, for example:
$ clang++ -std=c++20 -Wall -Wextra -pedantic -c packet.cpp -o /tmp/packet.o
$ git diff --check
$ git diff -- packet.cpp
The compile command is ordinary and can run as your normal user. Use sudo only if your project directory itself is inaccessible, and fix ownership or permissions rather than making the whole build run as root.
5. Apply the change only after the preview passes
Warning
-i overwrites the source file. It is an in-place edit, not a dry run. In a version-controlled checkout, inspect the working tree first:
$ git status --short
$ cp --preserve=all packet.cpp packet.cpp.before-reorder
$ clang-reorder-fields-20 -i \
--record-name=Packet \
--fields-order=kind,flags,payload \
packet.cpp
$ git diff --check
$ git diff -- packet.cpp
If the preview was written to packet.cpp.new, you can instead promote it after review:
$ cmp -s packet.cpp.new packet.cpp; echo "comparison status: $?"
$ mv packet.cpp.new packet.cpp
If the in-place result is wrong, restore the backup before doing anything else:
$ cp --preserve=all packet.cpp.before-reorder packet.cpp
$ git diff -- packet.cpp
Once the new version is compiled and tested, remove the backup only as a deliberate cleanup step. That deletion is irreversible outside version control.
6. Diagnose the likely failures
A missing or misspelled record name means the tool cannot find the definition. Check namespaces and spelling rather than guessing. A field-order list with the wrong number of names is invalid; do not separate names with spaces, because each space-separated value is treated as another command-line argument or source path.
Some declarations are not safe to rewrite. Preprocessor directives between fields, macros that expand to multiple fields, and other unsupported forms can prevent reordering. A flexible array member in C must remain last, so an order that moves it should be rejected. Treat such diagnostics as a reason to review the design manually, not as an invitation to force the edit.
If the command reports a compiler diagnostic or a constructor-initialiser warning, preserve it with the preview and investigate the source location. A zero exit status means the tool completed its rewrite; it does not prove that the resulting program is correct.
Done means
- The installed executable and package version were checked.
- The record name and comma-separated field order contain exact, complete names.
- A preview was reviewed before any in-place edit.
- Compiler context was supplied with
-por an explicit extra argument when required. - Initialisers, layout assumptions and warnings were reviewed.
- The changed source passes the project's compile and test checks, and recovery instructions are available.