Curated internet, served daily

Clang's Two-Fixit Warnings Come in an Order It Never Explains

Clang sometimes prints two fixits under one warning — one that changes behavior, one that silences the note — and the order is a design decision it never states, which I checked by compiling four cases on my own machine.

Research

GCC 4.1 looked at a struct definition with no trailing semicolon and reported that the declaration contained multiple types. GCC 4.6 said it expected ’;’ after struct definition, and pointed at the position where it believed the semicolon belonged. GCC 7 added a fixit on top of the diagnosis.

test.cpp:1:12: error: expected ';' after struct definition
struct S {}
          ^
          ;

Symptom, then diagnosis, then prescription. Arthur O’Dwyer laid that progression out in a 2020 post built around two peripheral parts of diagnostic design: fixits and suppression mechanisms.

The second half concerns warnings rather than errors, and its example is an assignment used as a condition, if (argc = 1). Every mainstream compiler flags it — GCC and Clang under -Wall, MSVC under -W4 — and GCC’s text is worth reading slowly:

warning: suggest parentheses around assignment used as truth value [-Wparentheses]
    4 | if (argc = 1)
      |    ~~~~~^~~

The message states the suppression. It never states the diagnosis, which is that people mean argc == 1 and a finger slips, and that the result compiles fine anyway.

The extra pair of parentheses is a suppression mechanism: an arbitrary way of telling the compiler the legal but unusual construct was intended. The same shape turns up in the (void) cast that quiets -Wunused-value and -Wunused-result, and in the parens -Wlogical-op-parentheses wants around a && b || c.

The musing that held me was about fixits that cannot do both jobs. When the compiler believes the behavior and the intent have come apart, a one-click fix has to choose: change the behavior to what was probably meant, or keep the behavior and silence the note.

Clang can print both, one per note. The order it prints them in is what I wanted to check.

Setup

One file, two compilers, no flags beyond the warning level.

#include <cstring>

int main(int argc, char **argv) {
    if (argc = 1) return 1;
    return 0;
}

int foo();
int assign_in_cond(int x) { if (x = foo()) return 1; return 0; }

int logical(int a, int b, int c) { return (a && b || c); }

int precedence(int mask) { return (foo() & mask != 0); }

int memsize(const char *a, const char *b, int len) {
    return strncmp(a, b, len < 0);
}

int notcmp(int x, int y, int z) { return x == y && !x == z; }
g++ -Wall -c test.cpp
clang++ -Wall -c test.cpp

Then read the note lines in the order they print, top to bottom. That is the whole method. I kept one warning per function so the note pairs would not tangle with each other, and built once with -Werror to confirm the warnings were live. On Windows the same file goes through MSVC at -W4, which the post lists beside -Wall for the assignment case.

Findings

The assignment case is the cleanest, because the warning never names its diagnosis. GCC asks for parentheses, and parentheses are the suppression. A diagnostic whose text is a recipe for silencing it is a strange artifact; the post calls it passive-aggressive, and that is the right word.

The four two-note cases in my file split two ways. On if (x = foo()) the first note says to place parentheses around the assignment to silence the warning, and the second says to use ’==’ to turn the assignment into an equality comparison. Suppression first.

On foo() & mask != 0 the first note wraps the ’!=’ to silence, the second wraps the ’&’ so it evaluates first. Suppression first again.

Then the order flips. On strncmp(a, b, len < 0) the first note asks whether the result of ‘strncmp’ should be compared instead — a behavior change — and the second offers an explicit cast of the argument to size_t to silence it.

On x == y && !x == z the first adds parentheses after the ’!’ so the comparison evaluates first, and the second wraps the left-hand side to silence. Treatment first, twice.

So two of the four lead with the suppression and two lead with the treatment, and nothing in the output says why. The logical case got one note, the parens, with nothing to weigh against it. Four cases is a small stage for a habit that fires on every build.

My own run added a detail I did not expect. GCC produced a single fixit across the four cases, on the last of them, and that fixit was the suppression. The compiler most willing to describe a treatment in prose is the one that will not put a treatment behind a keystroke.

The consequence is small and sharp. A fixit is one keystroke, and whichever note prints first is the one sitting under the cursor. On if (x = foo()) the cheap click keeps the assignment and quiets the warning. On strncmp the cheap click changes what the call compares.

Clang placed opposite bets about which mistake it was looking at, and labeled neither. If I wanted a rule for the ordering, it would need a prior: for this warning, in this codebase, is the typo more common than the deliberate oddity? The compiler has no such prior and does not claim one. It has a line of text and a keystroke, and the note order is where the guess leaks out.

The second musing is what makes the bet legible. Warnings of this kind fire when the written code and the intended code sit close together — a dropped ’=’ or a missing pair of parens.

(!a)==(b) plainly means one reading, a != b plainly means the other, and (!a==b) falls between them. Suppression parens are how a programmer writes: I know, I meant it.

That turns a suppression into a signal, and it also explains why the compiler cannot trust it. A deliberate suppression and an unread warning look identical from the outside. Both are someone adding parentheses until the build goes quiet.

What changed for me is smaller than a rule. A warning with two notes is a short argument about intent, printed in an order the compiler chose and did not state. I read the second note before pressing anything now. The habit paid for itself on the first assignment I had actually meant as a comparison.

Boundaries

Four cases, one file, two compilers on my own machine. I did not run GCC 4.1, 4.6 or 7; the version history and the diagnostic text in Research come from the post. The four ordering examples are the whole sample, and I did not go hunting for more two-note pairs.

Nothing here measures how often a programmer clicks the first note and moves on. MSVC at -W4 went untested. The distance-between-readings argument stays qualitative — I have no way to score how far (!a==b) sits from either reading. None of this establishes which order is correct. It establishes that the order is a decision, and that the output does not disclose it.

Verdict

Read it if you write diagnostics: a compiler, a linter, a type checker, the error strings in a tool people hit all day. The two-fixit problem is a design problem before it is a C++ problem, and most tools settle it by accident.

Skip it if you want a rule for choosing between the two notes. There isn’t one, and the post does not pretend otherwise.

The narrow build lesson is a compiler that keeps both options, prints both, and chooses the order on purpose. GCC’s smaller choice — a single fixit, and it is the suppression — is defensible for the same reason: it never pushes a behavior change at anyone.

The picture I keep is a build log with one warning and two notes beneath it, cursor resting on the first line. Read the second note before pressing enter.

← Back to Deep Dives