wake-up-neo.com

Aliasing von Struktur und Array auf C++

Dies ist eine C++ - Folge für eine andere Frage von mir

In den alten Tagen von Pre-ISO C hätte der folgende Code niemanden überrascht:

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);
}

Leider verwendet diese problematische Zeile die Zeigerarithmetik (p[i] ist per Definition*(p + i)) außerhalb eines Arrays, das vom Standard ausdrücklich nicht zulässig ist.) Draft 4659 für C++ 17 sagt in 8.7 [expr.add]:

Wenn der Ausdruck P auf das Element x [i] eines Arrayobjekts x mit n Elementen zeigt, wird die Ausdrücke P + J und J + P (wobei J den Wert j hat) zeigen auf das (möglicherweise hypothetische) Element x [i + j] wenn 0 <= i + j <= n; Andernfalls ist das Verhalten undefiniert.

Und die (nicht normative) Anmerkung 86 macht es noch deutlicher:

Ein Objekt, das kein Array-Element ist, wird für diesen Zweck als zu einem Array mit einem Element gehörig betrachtet. EIN Ein Zeiger hinter dem letzten Element eines Arrays x von n Elementen wird als äquivalent zu einem Zeiger auf ein hypothetisches Element angesehen. x [n] zu diesem Zweck.

Die akzeptierte Antwort auf die verwiesene Frage nutzt die Tatsache, dass die C-Sprache type punning durch Unionen akzeptiert, aber ich konnte niemals das Äquivalent im C++ - Standard finden. Ich gehe also davon aus, dass eine Union, die ein anonymes struct-Mitglied und ein Array enthält, in C++ zu Undefined Behaviour führen würde - sie are verschiedenen Sprachen ...

Frage:

Was könnte ein konformer Weg sein, Mitglieder einer Struktur so zu durchlaufen, als wären sie Mitglieder eines Arrays in C++? Ich suche einen Weg in aktuellen (C++ 17) Versionen, aber auch Lösungen für ältere Versionen sind willkommen.

Haftungsausschluss:

Dies gilt natürlich nur für Elemente desselben Typs, und das Auffüllen kann mit einer einfachen assert erkannt werden, wie in der anderen Frage gezeigt. Daher sind Auffüllen, Ausrichtung und gemischte Typen hier nicht mein Problem.

25
Serge Ballesta

Verwenden Sie ein constexpr-Array mit Zeiger auf Mitglied:

#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);
}
23
Tobi

IMHO ist der einfachste Weg, einfach operator[] zu implementieren. Sie können ein Helper-Array wie dieses erstellen oder einfach einen Schalter erstellen ...

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;
}
16
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);
  }
};

dies setzt voraus, dass zwischen den doubles in Ihrer `struct keine Packung vorhanden ist. Das zu behaupten ist schwierig.

Eine POD-Struktur ist eine garantierte Folge von Bytes.

Ein Compiler sollte in der Lage sein, [] auf die gleichen Anweisungen (oder deren Fehlen) zu kompilieren wie ein reiner Array-Zugriff oder eine Zeigerarithmetik. Es gibt may Probleme, bei denen diese Optimierung "zu spät" für andere Optimierungen geschieht.

Es ist möglich, dass die Konvertierung in char* oder std::byte* insted von uintptr_t gültig ist, aber es gibt ein Kernproblem darüber, ob in diesem Fall Zeigerarithmetik zulässig ist.

Sie können die Tatsache verwenden, dass das Umsetzen eines Zeigers auf intptr_t, das Arithmetik ausführt, und dann den Wert zurück auf den Zeigertyp umwandelt, implemetiertes Verhalten Ich glaube, dass es auf den meisten Compilern funktionieren wird: 

template<class T>
T* increment_pointer(T* a){
  return reinterpret_cast<T*>(reinterpret_cast<intptr_t>(a)+sizeof(T));
  }

Diese Technik ist die effizienteste, Optimierer scheinen anscheinend nicht optimal zu produzieren, wenn man die Tabellensuche verwendet: Assemblys-Vergleich

0
Oliv