Предположим, я хочу написать собственный класс vec3, который хранит три числа с плавающей запятой в виде массива (или вектора в математическом смысле) на C++, и я ожидаю, что он будет соответствовать следующим спецификациям:
- Его отдельные члены должны быть доступны напрямую, как v.x, а не через средства доступа, такие как v.x(), поскольку я хочу использовать его взаимозаменяемо с, скажем, glm::vec3.
- Он должен иметь оператор[], который возвращает соответствующий элемент в этом измерении, а возвращаемое значение должно иметь ссылочный тип float&, поскольку Я хочу использовать его в общем контексте, который принимает любой динамически индексируемый тип, подобный массиву.
- Его оператор[] реализация должна быть максимально производительной, поскольку ее можно вызывать чертовски много раз каждую миллисекунду.
- Это должно быть тривиально- копируемые, а x, y и z должны иметь последовательные адреса, так как я хочу напрямую скопировать массив vec3 в память графического процессора и использовать их в шейдере или чем-то еще.
- Его реализация оператора[] не должна вызывать UB.
Код: Выделить всё
leaРеализация:
Код: Выделить всё
struct vec3 {
union {
struct {
float x;
float y;
float z;
};
float xyz[3];
};
[[gnu::used]] constexpr float& idx0(size_t i) {
// UB as violating the strict aliasing rule
return reinterpret_cast(this)[i];
}
[[gnu::used]] constexpr float& idx1(size_t i) {
// The xyz variant is only used here
// UB as accessing union from potentially non-active member
return xyz[i];
}
};
Код: Выделить всё
vec3::idx0(unsigned long):
lea rax, [rdi + 4*rsi]
ret
vec3::idx1(unsigned long):
lea rax, [rdi + 4*rsi]
ret
Код: Выделить всё
struct vec3 {
// same data members as above
[[gnu::used]] constexpr float& idx2(size_t i) {
if (i == 0) {
return x;
} else if (i == 1) {
return y;
} else if (i == 2) {
return z;
} else {
std::unreachable();
}
}
[[gnu::used]] constexpr float& idx3(size_t i) {
switch (i) {
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
std::unreachable();
}
}
[[gnu::used]] constexpr float& idx4(size_t i) {
auto addrs = std::array{&x, &y, &z};
return *addrs[i];
}
[[gnu::used]] constexpr float& idx5(size_t i) {
auto accessors = std::array{&vec3::x, &vec3::y, &vec3::z};
return this->*accessors[i];
}
};
Код: Выделить всё
vec3::idx2(unsigned long):
mov rax, rdi
test rsi, rsi
je .LBB2_4
cmp rsi, 2
jne .LBB2_2
add rax, 8
.LBB2_4:
ret
.LBB2_2:
add rax, 4
ret
vec3::idx3(unsigned long):
mov rax, rdi
test rsi, rsi
je .LBB3_4
cmp rsi, 2
jne .LBB3_2
add rax, 8
.LBB3_4:
ret
.LBB3_2:
add rax, 4
ret
vec3::idx4(unsigned long):
mov qword ptr [rsp - 24], rdi
lea rax, [rdi + 4]
mov qword ptr [rsp - 16], rax
add rdi, 8
mov qword ptr [rsp - 8], rdi
mov rax, qword ptr [rsp + 8*rsi - 24]
ret
vec3::idx5(unsigned long):
mov rax, rdi
lea rcx, [rip + .L__const.vec3::idx5(unsigned long).accessors]
add rax, qword ptr [rcx + 8*rsi]
ret
.L__const.vec3::idx5(unsigned long).accessors:
.quad 0
.quad 4
.quad 8
- Есть ли какой-нибудь способ проинструктировать компиляторы генерировать идеальный код, с реализацией, написанной на «стандартном» C++ без UB? Возможно, я просто не нашел правильный способ сообщить составителям дополнительную информацию.
- Если нет, то это из-за ограничения языковых стандартов или это из-за отсутствия в компиляторах определенных методов оптимизации? В случае с idx5 компиляторы могли бы определить, что загруженное смещение всегда имеет то же значение, что и 4*i.
- Есть ли какое-либо определенное изменение (или какое-либо существующее предложение) в стандарте C++, которое могло бы помочь в этой ситуации? Самое близкое, что я могу придумать, это следующее: решат ли они проблему?
Разрешение доступа неактивным членам профсоюза, чьи все члены представляют собой тривиально копируемые типы без дополнений. - Дождитесь реализации std::start_lifetime_as и используйте это для реализации оператора[] как idx6.
Подробнее здесь: https://stackoverflow.com/questions/792 ... rator-in-c