April 3rd, 2023

C++17 creates a practical use of the backward array index operator

It is well-known that if a is a pointer or array and i is an integer, then a[i] and i[a] are equivalent in C and C++, resulting in hilarity like

void haha()
{
    int a[5];
    for (i = 0; i < 5; i++) {
        i[a] = 42;
    }
}

There is very little practical use for this equivalency, aside from pranking people.¹

And then C++17 happened.

One of the changes to the core language in C++17 was stronger order of evaluation rules, formally known as sequencing. We previously encountered this when studying a crash that seemed to be on a std::move operation.

One of the operations that received a defined order of evaluation is the subscript operator. Starting in C++17, a[b] always evaluates a before evaluating b.

int* p;
int index();

auto test()
{
    return p[index()];
}

// Compiled as C++14

    sub     rsp, 40
    call    index       ; call index first
    movsxd  rcx, rax
    mov     rax, p      ; then fetch p
    mov     eax, [rax + rcx * 4]
    add     rsp, 40
    ret

// Compiled as c++17

    push    rbx
    sub     rsp, 32
    mov     rbx, p      ; fetch p first
    call    index       ; then call index
    movsxd  rcx, rax
    mov     eax, [rbx + rcx * 4]
    add     rsp, 32
    pop     rbx
    ret

Therefore, if your evaluation of the index may have a side effect on the evaluation of the pointer, you can flip the order to force the index to be calculated first.

auto test()
{
    return index()[p];
}

Astound your friends! Confuse your enemies!

Bonus chatter: Though I wouldn’t rely on this yet. clang implements this correctly, but msvc (v19) and gcc (v13) get the order wrong and still load p before calling index. (By comparison, icc also gets the order wrong, but the other way: It always loads p last.)

¹ Another practical use is to bypass any possible overloading of the [] operator, as noted in Chapter 14 of Imperfect C++:

#define ARRAYSIZE(a) (sizeof(a) / sizeof(0[a]))

By flipping the order in 0[a], this bypasses any possible a[] overloaded.

std::vector<int> v(5);
int size = ARRAYSIZE(v); // compiler error

However, it isn’t foolproof. You just need to create a more clever fool: If v is a pointer or an object convertible to a pointer, then that pointer will happily go inside the 0[...].

struct Funny
{
    operator int*() { return oops; }
    int oops[5];
    int extra;
};

Funny f;
int size1 = ARRAYSIZE(f); // oops: 6

int* p = f;
int size2 = ARRAYSIZE(p); // oops: 1

Fortunately, you don’t need any macro tricks. You can let C++ constexpr functions do the work for you:

template<typename T, std::size_t N>
constexpr std::size_t array_size(T(&)[N]) { return N; }
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • indranil chakraborty

    For the last piece of code, I’d suggest using std::size()

  • Neil Rashbrook

    I once wanted to have a typesafe constant arraysize in a compiler without constexpr (probably VC++ 2010 or earlier) so I had to have the worst of both worlds: a macro wrapper around a templated type.