Why is GCC wrongly detecting shift count overflow?

Viewed 156

I am trying to make a compile-time set of tags at most 8 chars out of constant string. For example, "abc" should become 0x6162630000000000, "" should become 0, etc.

For this, I thought I should use the fact that references to arrays do not decay to pointers and I can capture the N (size) of the const array with this:

template <size_t N>
inline constexpr uint64_t make_tag(char const (&arr)[N]) {
    if (N == 1) {
        return 0;
    }

    uint64_t result = 0;

    for (size_t i = 0; (i < (N-1)) && (i<8); i++) {
        result = (result << 8) | arr[i];
    }

    if ((N-1) < 8) {
        result = result << (8 * (8 - (N-1)));
    }

    return result;
}

The N is captured correctly which is 1 for "" and len+1 for the rest of strings.

Here is the thing that confuses me: when doing something like

static constexpr uint64_t a0=make_tag("");

GCC 11.2 is telling me this:

<source>: In instantiation of 'constexpr uint64_t make_tag(const char (&)[N]) [with long unsigned int N = 1; uint64_t = long unsigned int]':
<source>:24:43:   required from here
<source>:17:33: error: left shift count >= width of type [-Werror=shift-count-overflow]
   17 |                 result = result << (8 * (8 - (N-1)));
      |                          ~~~~~~~^~~~~~~~~~~~~~~~~~~~
cc1plus: all warnings being treated as errors

clang does not behave like that.

For convenience, here is the compiler explorer snippet:

EDIT: if (N == 1) {...} else {...} did the trick as @Useless suggested

1 Answers

Why is GCC wrongly detecting shift count overflow?

It isn't.

When N=1, the code

    if ((N-1) < 8) {
        result = result << (8 * (8 - (N-1)));
    }

results in a 64-bit shift.

If you don't want that code compiled for N=1, make it explicit:

    if (N > 1 && (N-1) < 8) {
        result = result << (8 * (8 - (N-1)));
    }

Putting everything inside the else branch of the initial check if (N ==1) { return 0; } else { ... works too.

It's unclear why GCC doesn't propogate the constraint on N implied by the early return without that, but I suspect it's simply not required.

Note the standard language around if constexpr says:

If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement.

There's nothing there about statically discarding anything after the if constexpr statement, even if the taken branch has an early return. Of course it could be optimized out, but it still needs to be compilable in the first place.

Related