Смещение структуры и массива C++

Это C++ наблюдение за другим вопросом моего

В прежние дни до ISO C следующий код никого не удивил бы:

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

К сожалению, эта проблемная строка использует арифметику указателя (p[i] по определению *(p + i)) вне любого массива, который явно не допускается стандартом. Проект 4659 для C++ 17 говорит в 8.7 [expr.add]:

Если выражение P указывает на элемент x [i] объекта массива x с n элементами, выражения P + J и J + P (где J имеет значение j) указывают на (возможно-гипотетический) элемент x [i + j], если 0 <= я + j <= n; в противном случае поведение не определено.

И (ненормативная) заметка 86 делает ее еще более явной:

Для этой цели считается, что объект, который не является элементом массива, относится к одноэлементному массиву. Указатель, предшествующий последнему элементу массива x из n элементов, считается эквивалентным указателю на гипотетический элемент x [n] для этой цели.

В принятом ответе упомянутого вопроса используется тот факт, что язык C принимает тип, пишущий через союзы, но я никогда не мог найти эквивалент в стандарте C++. Поэтому я предполагаю, что объединение, содержащее анонимный элемент структуры и массив, приведет к Undefined Behaviour в C++ - это разные языки...

Вопрос:

Что может быть подходящим способом для итерации через члены структуры, как если бы они были членами массива в C++? Я ищу способ в текущих версиях (C++ 17), но также приветствуются решения для более старых версий.

Отказ от ответственности:

Очевидно, что это относится только к элементам того же типа, и отступы могут быть обнаружены с помощью простого assert как показано в этом другом вопросе, поэтому дополнения, выравнивание и смешанные типы здесь не являются моей проблемой.

Ответы

Ответ 1

Используйте массив constexpr от указателя к элементу:

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

Ответ 2

ИМХО самым простым способом является просто реализовать operator[]. Вы можете создать вспомогательный массив, подобный этому, или просто создать переключатель...

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

Ответ 3

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

это зависит от отсутствия упаковки между double в вашей структуре. Утверждение, что это сложно.

Структура POD - это гарантированная последовательность байтов.

Компилятор должен иметь возможность компилировать [] до тех же инструкций (или их отсутствия) в качестве доступа к необработанному массиву или арифметики указателя. Могут быть некоторые проблемы, когда эта оптимизация происходит "слишком поздно" для других оптимизаций, поэтому необходимо дважды проверить код, чувствительный к производительности.

Возможно, что преобразование в char* или std::byte* insted из uintptr_t было бы правильным, но есть основная проблема, если в этом случае разрешена арифметика указателя.

Ответ 4

Вы могли бы использовать тот факт, что приведение указателя к intptr_t выполняющего арифметику, а затем intptr_t значения обратно к типу указателя - это поведение, определяемое реализацией. Я считаю, что он будет работать на большинстве компиляторов:

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

Эта техника является наиболее эффективной, оптимизаторы, похоже, не могут создать оптимальные, если использовать таблицу поиска: сборка-сравнение