удобный класс Vector3f

Иногда возникает необходимость иметь класс Vector3f, который имеет элементы x, y и z и может быть индексирован как массив float[3] одновременно (здесь, в SO, уже есть несколько вопросов по этому поводу).

Что-то вроде:

struct Vector3f {
    float data[3];
    float &x = data[0];
    float &y = data[1];
    float &z = data[2];
};

С этим мы можем написать это:

Vector3f v;
v.x = 2.0f;
v.y = 3.0f;
v.z = 4.0f;
glVertex3fv(v.data);

Но эта реализация плохая, потому что ссылки занимают место в struct (что весьма прискорбно. Я не вижу причин, по которым ссылки нельзя удалить в данном конкретном случае, возможно, это упущенная оптимизация со стороны компилятора).

Но с [[no_unique_address]] у меня возникла такая идея:

#include <new>

template <int INDEX>
class Vector3fProperty {
    public:
        operator float() const {
            return propertyValue();
        }
        float &operator=(float value) {
            float &v = propertyValue();
            v = value;
            return v;
        }
    private:
        float &propertyValue() {
            return std::launder(reinterpret_cast<float*>(this))[INDEX];
        }
        float propertyValue() const {
            return std::launder(reinterpret_cast<const float*>(this))[INDEX];
        }
};

struct Vector3f {
    [[no_unique_address]]
    Vector3fProperty<0> x;
    [[no_unique_address]]
    Vector3fProperty<1> y;
    [[no_unique_address]]
    Vector3fProperty<2> z;

    float data[3];
};

static_assert(sizeof(Vector3f)==12);

Итак, в основном у меня есть свойства в struct, который обрабатывает доступ к x, y и z. Эти свойства не должны занимать места, так как они пусты и имеют атрибут [[no_unique_address]].

Что вы думаете об этом подходе? У него есть УБ?


Обратите внимание, этот вопрос касается класса, для которого все это возможно:

Vector3f v;
v.x = 1;
float tmp = v.x;
float *c = v.<something>; // there, c points to a float[3] array

person geza    schedule 05.09.2019    source источник
comment
Думаю, это УБ. [[no_unique_address]] означает только то, что член не должен иметь уникальный адрес, а не то, что он не должен иметь уникальный адрес. Ссылка на родительский класс из дочернего также выглядит подозрительно.   -  person freakish    schedule 05.09.2019
comment
@freakish: ах, я это пропустил. Если это так, то это ответ.   -  person geza    schedule 05.09.2019
comment
эта реализация плохая, потому что ссылки занимают место в структуре - и в большинстве случаев это на самом деле не имеет значения.   -  person Jesper Juhl    schedule 05.09.2019
comment
@JesperJuhl: и бывают случаи, когда это очень важно.   -  person geza    schedule 05.09.2019
comment
@geza Итак, вам нужны геттеры / сеттеры, которые C ++ не поддерживает. Самое близкое, что вы можете получить, - это написать такие методы, как float& x() const; и void x(float);.   -  person freakish    schedule 05.09.2019
comment
почему бы не иметь v.data [0], v.data [1], v.data [2] и v.x (), v.y (), v.z ()?   -  person slepic    schedule 05.09.2019
comment
@JesperJuhl Бесполезный комментарий бесполезен. Тот факт, что могут быть конкретные приложения, в которых утроение размера вашего типа не имеет значения, не означает, что вам вообще не следует заботиться о реализации таких фундаментальных компонентов библиотеки, как эта.   -  person Barry    schedule 05.09.2019
comment
@Barry В общем, тебе не все равно. Но в моем опыте; что-то подобное редко бывает узким местом. Так что лучше просто жить своей жизнью и приносить пользу своим клиентам, а потом беспокоиться об этом, если это на самом деле проявляется как проблема.   -  person Jesper Juhl    schedule 05.09.2019
comment
@slepic: потому что это вызов функции. v.x()=2.0f выглядит некрасиво. v.setX(2.0f) не так кратко, как могло бы быть.   -  person geza    schedule 05.09.2019
comment
@JesperJuhl: хорошо. Предположим, я сделал это и обнаружил, что это узкое место. Что нетрудно представить. Без ссылок эта структура составляет 12 байтов. Со ссылками это 16 + 3 * 8 = вы делаете математику (это для 64-битных машин). Если у меня много векторов, это может иметь значение.   -  person geza    schedule 05.09.2019
comment
@geza Конечно, это может иметь значение. Я не сказал иначе. Я просто говорю, что это редко имеет значение в большинстве реальных программ. А когда этого не происходит, тратить время на оптимизацию - пустая трата времени, которую лучше было бы потратить на что-то другое.   -  person Jesper Juhl    schedule 05.09.2019
comment
В этом случае, @JesperJuhl, я лично предполагаю, что это имеет значение, на том основании, что массив из трех float, а не double, конкретно названных x, y и z, очень наводит на размышления о 3D-графике. Таким образом, он, вероятно, будет использоваться в контексте, где должно быть сохранено большое количество Vector3f (для представления вершин объекта), а производительность чрезвычайно важна (потому что она должна конкурировать с другими библиотеками трехмерной графики), и, таким образом, по существу, сохранение трех указателей будет быть крайне нежелательным.   -  person Justin Time - Reinstate Monica    schedule 05.09.2019
comment
Эти вызовы функций на самом деле не отличаются от operator[] концептуально, но я согласен, что v.x было бы более кратким. Реализация может стать даже уродливее;), но некоторым компиляторам нравится it даже больше, чем легко один.   -  person Bob__    schedule 05.09.2019
comment
@Bob__: нет разницы между уродливым и легким. Оба компилируются в mov eax, 3; рет. Просто norm_2 не удаляется, так как это не шаблон.   -  person geza    schedule 05.09.2019
comment
(В основном @JesperJuhl): Реализации могут меняться. Более важно иметь стабильный интерфейс. Поэтому ключевой вопрос: Если это окажется узким местом, можно ли изменить реализацию на что-то лучшее, не затрагивая клиентский код? Вопрос относится к glVertex3fv, но это это древний API. В настоящее время данные вершин хранятся в объектах буфера вершин (VBO), где структура данных может быть намного сложнее, чем у простого массива ...   -  person Marco13    schedule 06.09.2019


Ответы (4)


Но эта реализация плохая, потому что ссылки занимают место в структуре (что весьма прискорбно. Я не вижу причин, по которым ссылки нельзя удалить в этом конкретном случае, возможно, это упущенная оптимизация со стороны компилятора).

Это похоже на сложную проблему. Классы стандартной компоновки должны быть совместимы друг с другом. Таким образом, компиляторам не разрешается удалять какие-либо члены, независимо от того, как они определены. За нестандартную планировку? Кто знает. Для получения дополнительной информации прочтите это: Выполните Стандарты C ++ гарантируют, что неиспользуемые частные поля будут влиять на sizeof?

По моему опыту, компиляторы никогда не удаляют члены класса, даже если они «не используются» (например, формально sizeof их использует).

У него есть УБ?

Думаю, это УБ. Прежде всего [[no_unique_address]] означает только то, что член не должен иметь уникальный адрес, а не то, что он не должен иметь уникальный адрес. Во-вторых, неясно, где начинается ваш data член. Опять же, компиляторы могут использовать или не использовать дополнения предыдущих [[no_unique_address]] членов класса. Это означает, что ваши аксессоры могут получить доступ к неправильной части памяти.

Другая проблема заключается в том, что вы хотите получить доступ к «внешней» памяти из «внутреннего» класса. AFAIK такая вещь также UB в C ++.

Что вы думаете об этом подходе?

Если предположить, что это правильно (а это не так), мне все равно это не нравится. Вам нужны геттеры / сеттеры, но C ++ не поддерживает эту функцию. Поэтому вместо того, чтобы создавать эти странные, сложные конструкции (представьте, что другие люди поддерживают этот код), как насчет того, чтобы просто сделать

struct Vector3f {
    float data[3];
    float x() {
        return data[0];
    }
    void x(float value) {
        data[0] = value;
    }
    ...
};

Вы говорите, что этот код уродлив. Может быть это. Но он прост, его легко читать и поддерживать. Нет UB, он не зависит от потенциальных хаков с союзами и делает именно то, что вы хотите, за исключением требований красоты. :)

person freakish    schedule 05.09.2019
comment
Возможно, вам будет интересно, я попросил удалить часть члена класса здесь: stackoverflow.com/questions/57811424/ - person geza; 05.09.2019

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

class Vec3f {
public:
    float x;
    float y;
    float z;

    float &operator[](int i) {
        if(i == 0) {
            return x;
        }
        if(i == 1) {
            return y;
        }
        if(i == 2) {
            return z;
        }
    }
};

Я бросил это в Compiler Explorer (https://godbolt.org/z/0X4FPL), который показал лязг оптимизация operator[] вызова на -O2 и GCC на -O3. Менее увлекательный, чем ваш подход, но простой и должен работать в большинстве случаев.

person youngmit    schedule 05.09.2019
comment
Что ж, это будет оптимизировано только тогда, когда i будет известен во время компиляции, верно? godbolt.org/z/evVGPZ - person freakish; 05.09.2019
comment
Правда! То, что вы получаете в своем примере, определенно менее эффективно, чем оптимальное. Однако я полагаю, что в большинстве приложений с массивом фиксированного размера могут возникнуть ситуации, когда конкретный индекс не может быть известен во время компиляции. Либо индекс будет предоставлен как литерал, либо будут циклы по [0,2]. Это, конечно, очень чувствительно к приложению, поэтому анонимные структуры / объединения, предложенные @Xirema, вероятно, лучший способ пойти. - person youngmit; 05.09.2019

GLM реализует такую ​​функциональность, используя анонимные structs внутри анонимного union

Я не могу лично гарантировать, что это соответствует стандарту, но большинство основных компиляторов (MSVC, GCC, Clang) будут поддерживать эту идиому:

struct Vector3f {
    union {
        struct {
            float x, y, z;
        };
        struct {
            float data[3];
        };
    };
    Vector3f() : Vector3f(0,0,0) {}
    Vector3f(float x, float y, float z) : x(x), y(y), z(z) {}
};

int main() {
    Vector3f vec;
    vec.x = 14.5;
    std::cout << vec.data[0] << std::endl; //Should print 14.5
    vec.y = -22.345;
    std::cout << vec.data[1] << std::endl; //Should print -22.345
    std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

Нестандартное поведение заключается в анонимной структуре, используемой для группировки букв вместе, о чем GCC выдаст предупреждение. Насколько мне известно, сам union должен быть действительным, потому что все типы данных идентичны, но вы все равно должны сверяться с документацией к компилятору, если вы не уверены, действительно ли это или нет.

Для дополнительного удобства мы также можем перегрузить оператор скобок, чтобы немного сократить наш синтаксис:

struct Vector3f {
    /*...*/
    float& operator[](size_t index) {return data[index];}
    float operator[](size_t index) const {return data[index];}
};



int main() {
    Vector3f vec;
    vec.x = 14.5;
    std::cout << vec[0] << std::endl; //Should print 14.5
    vec.y = -22.345;
    std::cout << vec[1] << std::endl; //Should print -22.345
    std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

Для ясности, доступ к неактивным членам, как я, действителен в соответствии со стандартом C ++, потому что эти члены имеют общую подпоследовательность:

Если два члена объединения являются типами стандартного макета, вполне определено проверить их общую подпоследовательность на любом компиляторе.

Ссылка CPP: Декларация Союза

Поскольку x и data[0]

  • Оба floats,
  • Оба занимают одну и ту же память,
  • Оба являются стандартными типами макета, как их определяет стандарт,

Совершенно верно получить доступ к одному или другому, независимо от того, который в настоящее время активен.

person Xirema    schedule 05.09.2019
comment
Это УБ. C ++ (в отличие от C) не позволяет получить доступ к другим членам союза после инициализации одним. - person freakish; 05.09.2019
comment
@freakish Имеет место, если (и только если) типы неактивных членов объединения такие же, как у активных членов, и занимают ту же самую память. - person Xirema; 05.09.2019
comment
@Xirema технически это UB с точки зрения стандартов. Однако AFAIK все компиляторы решили определить поведение по причинам совместимости (в пределах разумного). Если вы не думаете, что совместимые типы делают это нормально ... - person Mgetz; 05.09.2019
comment
@freakish: Я думаю, что Ксирема права. В стандарте есть дополнительный пункт, разрешающий этот случай. - person Andreas H.; 05.09.2019
comment
это UB и использует два расширения GNU: а) анонимная структура, запрещенная ISO. б) каламбурные работы союзного типа. Не ВСЕ компиляторы поддерживают это, но коллекции GNU работают в нестрогом режиме. Xinera неправильно цитирует пункт, потому что он относится к объектам с общим началом, что здесь не так. - person Swift - Friday Pie; 05.09.2019
comment
это УБ по стандарту. - person Oblivion; 05.09.2019
comment
@Xirema ваша анонимная структура и float[3] не имеют общей подпоследовательности структуры. Ну давай же. - person freakish; 05.09.2019
comment
@ Swift-FridayPie В этом коде не используется каламбур. Однако анонимная структура не является ISO, согласовано. - person Xirema; 05.09.2019
comment
общая последовательность здесь не применяется ... она применялась бы, если бы оба члена были ctypes стандартного макета с эквивалентными объявлениями. Либо два массива, либо две структуры. Итак, это технически типичный каламбур, потому что способ выравнивания массива может быть (определен платформой) отличным от структуры ... особенно если в коде использовались определенные прагмы. - person Swift - Friday Pie; 05.09.2019
comment
@Xirema также эта часть стандарта не подразумевает, что вы можете преобразовывать одно в другое. Это означает лишь то, что вы можете их изучить (возможно, самостоятельно). - person freakish; 05.09.2019
comment
@freakish Но в этом коде нет приведения. float всегда рассматривается как float. - person Xirema; 05.09.2019
comment
@Xirema Я имел в виду приведение массива к анонимной структуре. Ярлык, вы понимаете, о чем я. Набирать текст. - person freakish; 05.09.2019
comment
Нашел путаницу: забыл обернуть data внутри своей собственной структуры. Теперь должно быть хорошо. - person Xirema; 05.09.2019
comment
@Xirema по-прежнему UB. Компиляторам разрешено добавлять отступы между членами. Хотя я согласен, в этом случае большинство не будет (что делает случай особенным, мы не любим особенного). Интересно, с __attribute__((packed)) (или эквивалентом) это нормально. Хотя это действительно выглядит излишне сложным. - person freakish; 05.09.2019
comment
Возможно, стоит сравнить с std::complex, это, вероятно, самый близкий к вашему коду гарантированно совместимый пример. (Для сравнения, std::complex<T> требует, чтобы T был одним из трех типов с плавающей запятой, и по стандарту требуется, чтобы макет был идентичен T[2], без заполнения или других элементов, чтобы гарантировать совместимость макета с T _Complex C.) Это не ' Тем не менее, я не использую объединение или написание текста. - person Justin Time - Reinstate Monica; 05.09.2019

Как уже говорилось, это невозможно: арифметика указателей определяется только внутри массива, и нет способа (без указания ссылки в классе, который занимает место в текущих реализациях), чтобы v.x ссылался на элемент массива.

person Davis Herring    schedule 05.09.2019