> When C++17 changed the abstract pointer model, it changed all non-null
> type-punned pointers from "points to no object (but still denotes an
> address)"

Not sure that before C++17 we had "pointers to no object"

C++14 [basic.compound]/3 says:
[...] A valid value of an object pointer type represents either the address of a byte in memory or a null pointer. If an object of type T is located at an address A, a pointer of type cv T* whose value is the address A is said to point to that object, regardless of how the value was obtained. [...]

I interpret this as having an implied "...otherwise, an object pointer does not point to anything" at the end.
 

> to "points to an object (just not the one the pointer type
> suggests)". In doing so, it accidentally made pointer arithmetic ([expr.add]
> <https://timsong-cpp.github.io/cppwp/expr.add>) on type-punned pointers
> well-defined (but unimplementable when sizeof(T) != sizeof(element) for a
> T*).
>
> For example:
> #include <iostream>
> short* cast(long* p) { return reinterpret_cast<short*>(p); }
> int main() {
>      long a[2];
>      // gcc says 0. Standard says 1.
>      std::cout << (cast(a) + 1 == (void*)(a + 1)) << "\n";
>      // gcc says 4 on x86-64. Standard says 1.
>      std::cout << (cast(a + 1) - cast(a)) << "\n";
> }

The standard says UB. https://timsong-cpp.github.io/cppwp/n4659/expr.add#6

 Ah, I missed that! There is no issue, then. Thank you.