Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aliasing struct and array the C++ way

This is a C++ followup for another question of mine

In the old days of pre-ISO C, the following code would have surprised nobody:

struct Point {
    double x;
    double y;
    double z;
};
double dist(struct Point *p1, struct Point *p2) {
    double d2 = 0;
    double *coord1 = &p1->x;
    double *coord2 = &p2->x;
    int i;
    for (i=0; i<3; i++) {
        double d = coord2[i]  - coord1[i];    // THE problem
        d2 += d * d;
    }
    return sqrt(d2);
}

Unfortunately, this problematic line uses pointer arithmetic (p[i] being by definition *(p + i)) outside of any array which is explicitely not allowed by the standard. Draft 4659 for C++17 says in 8.7 [expr.add]:

If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i + j] if 0 <= i + j <= n; otherwise, the behavior is undefined.

And the (non-normative) note 86 makes it even more explicit:

An object that is not an array element is considered to belong to a single-element array for this purpose. A pointer past the last element of an array x of n elements is considered to be equivalent to a pointer to a hypothetical element x[n] for this purpose.

The accepted answer of the referenced question uses the fact that the C language accepts type punning through unions, but I could never find the equivalent in the C++ standard. So I assume that a union containing an anonymous struct member and an array would lead to Undefined Behaviour in C++ — they are different languages...

Question:

What could be a conformant way to iterate through members of a struct as if they were members of an array in C++? I am searching for a way in current (C++17) versions, but solutions for older versions are also welcome.

Disclaimer:

It obviously only applies to elements of same type, and padding can be detected with a simple assert as shown in that other question, so padding, alignment, and mixed types are not my problem here.

like image 546
Serge Ballesta Avatar asked Jan 26 '18 14:01

Serge Ballesta


3 Answers

Use an constexpr array of pointer-to-member:

#include <math.h>

struct Point {
    double x;
    double y;
    double z;
};

double dist(struct Point *p1, struct Point *p2) {
    constexpr double Point::* coords[3] = {&Point::x, &Point::y, &Point::z};

    double d2 = 0;
    for (int i=0; i<3; i++) {
        double d = p1->*coords[i] - p2->*coords[i];
        d2 += d * d;
    }
    return sqrt(d2);
}
like image 187
Tobi Avatar answered Nov 14 '22 04:11

Tobi


IMHO the easiest way is to just implement operator[]. You can make a helper array like this or just create a switch...

struct Point
{
    double const& operator[] (std::size_t i) const 
    {
        const std::array coords {&x, &y, &z};
        return *coords[i];
    }

    double& operator[] (std::size_t i) 
    {
        const std::array coords {&x, &y, &z};
        return *coords[i];
    }

    double x;
    double y;
    double z;
};

int main() 
{
    Point p {1, 2, 3};
    std::cout << p[2] - p[1];
    return 0;
}
like image 17
Jaa-c Avatar answered Nov 14 '22 05:11

Jaa-c


struct Point {
  double x;
  double y;
  double z;
  double& operator[]( std::size_t i ) {
    auto self = reinterpret_cast<uintptr_t>( this );
    auto v = self+i*sizeof(double);
    return *reinterpret_cast<double*>(v);
  }
  double const& operator[]( std::size_t i ) const {
    auto self = reinterpret_cast<uintptr_t>( this );
    auto v = self+i*sizeof(double);
    return *reinterpret_cast<double const*>(v);
  }
};

this relies on there being no packing between the doubles in your `struct. Asserting that is difficult.

A POD struct is a sequence of bytes guaranteed.

A compiler should be able to compile [] down to the same instructions (or lack thereof) as a raw array access or pointer arithmetic. There may be some problems where this optimization happens "too late" for other optimzations to occur, so double-check in performance sensitive code.

It is possible that converting to char* or std::byte* insted of uintptr_t would be valid, but there is a core issue about if pointer arithmetic is permitted in this case.

like image 2
Yakk - Adam Nevraumont Avatar answered Nov 14 '22 03:11

Yakk - Adam Nevraumont