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; }
For the last piece of code, I’d suggest using std::size()
This sort of thing is why Hyman Rosen authored this paper back in 2016.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0431r0.htm
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.