Как стать автором
Обновить

Комментарии 37

Если использовать clang, то есть удобный экстеншн (https://clang.llvm.org/docs/LanguageExtensions.html#vectors-and-extended-vectors), который позволяет объявлять векторные типы с такой же семантикой, как в OpenCL. Именно этот экстеншн эппл использует, чтобы имлементировать векторные типы в simd фреймворке.

Работает через typedef/using с аттрибутом ext_vector_type(N):

#if __has_attribute(ext_vector_type)
using float2 = float __attribute__((ext_vector_type(2)));
using float3 = float __attribute__((ext_vector_type(3)));
using float4 = float __attribute__((ext_vector_type(4)));

using int2 = int __attribute__((ext_vector_type(2)));
using int3 = int __attribute__((ext_vector_type(3)));
using int4 = int __attribute__((ext_vector_type(4)));
#endif

Да, это не стандартное поведение а специфичный для компилятора экстеншн, но зато работает без магии на макросах, да и вовсе без какого-либо дополнительного кода. И транслируется в нативные simd инструкции автоматически.

У gcc есть подобный экстеншн и аттрибут для объявления векторных типов, но swizzle там не поддерживается.

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

А чем такая запись

b = vec.xyz;  // b is now (1.0, 2.0, 3.0)
d = vec[2];   // d is now 3.0
a = vec.xxxx; // a is now (1.0, 1.0, 1.0, 1.0)

лучше чем такая?

b = vec.compose(0,1,2);
d = vec.compose(2);
a = vec.compose(0,0,0,0);

читабельностью?

Особенно когда есть xyzs, rgba, uvwt. То можно ругательства получать из последовательности символов. Но кроме эстетического удовлетворения никакой практической пользы не видно.
Так тоже не читабельно?

enum { R,G,B }; enum { X,Y,Z,S };
b = vec.compose(R,G,B);
d = vec.compose(Z);
a = vec.compose(X,X,X,X);

ну ругательства можно получить даже из слова "счастье", если записать его буквами "a, п, о, ж"
ориентировались всеже на шейдерный синтаксис, где .xyzw канонична
a = vec.xxxx;

Признаюсь, не прочитал полностью, но вообще не понял как это планировалось делать через union, ведь даже игнорируя УБ, xxx из xyz реинтерпретацией байт мягко говоря сложно( не говоря уже о xyzxyz)

Я бы сделал это consteval функцией, которая возвращает пак из указателей на филды(ну или просто индексы). Далее раскрываем пак складывая из вектора в vec<N> какой-то, ведь нужно любого размера. Выглядело бы это как-то так

vec<5> x = swizzle<"xxxyy">(v);

Но синтаксис уже какой хотите такой и добавляйте. В целом как-то не вижу где проблемы могут возникнуть в этой задаче(кроме того что вектора<N> может быть не предусмотрено и тогда придётся либо ограничиться массивом, когда размер != 2 или 3)

P.S. делать макросы из 2/3/4 букв без префиксов это конечно сильно, в моём сценарии это были бы либо переменные, либо функции, вызывающие нужный swizzle

ну задачи получать неразмерные вектора вида xxxyy не стояло, потому что работать с ними не получится, а вот .xxyy из .xy вполне можно, компилятор это сводит в итоге к операциям с элементами массивов. Все это давно пройдено в компиляторах шейдеров

Честно говоря, выглядит все это довольно хрупко. Тут целых два сомнительных места: во-первых, в C++ нельзя, записав в один элемент union, читать из другого (это UB), а во-вторых, структура и массив строго говоря не являются layout compatible, так что то, что это все сейчас работает - не более чем удачное совпадение. В компилятор добавят какие-нибудь новые оптимизации, как уже бывало, и все это поломается.

почему же, вполне себе работает, не только у нас но и у Unreal 4/5
https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Core/Public/Math/Vector.h#L50

template<typename T>
struct TVector
{
	static_assert(std::is_floating_point_v<T>, "T must be floating point");

public:
	using FReal = T;

	union   <<<<<<<<<<<<<<<<<<<< все это свернуто в юнион для удобства
	{
  		struct                               <<<<<<<<<<  поля xyz
		{
			/** Vector's X component. */
			T X;

			/** Vector's Y component. */
			T Y;

			/** Vector's Z component. */
			T Z;
		};

		UE_DEPRECATED(all, "For internal use only")
		T XYZ[3];                              <<<<<<<<<<<<< массив
	};

почему же, вполне себе работает

Еще раз: это UB. То, что это пока работает, вообще не показатель. Поработает-поработает, и перестанет. Вообще, на тему сделать структуры, которые были бы layout compatible с соответствующими массивами, есть пропозалы, например этот. Но на данный момент это все-таки UB.

тогда скорее уж узаконенная практикой вещь, потому что очень много кода работает вот с такой конвертацией, и это легальный способ представления float <-> int
union {
uint32_t i;
float f;
}
v;
и запрет его в новых версиях поломает кучу софта, на что вендоры пойдут в крайнем случае, скорее всего никогда

и запрет его в новых версиях поломает кучу софта, на что вендоры пойдут в крайнем случае, скорее всего никогда

Ну, может быть и так. А может быть и нет. В пользу последнего говорит например то что вместо "узаконивания" type punning через union в C++20 добавили std::bit_cast(). В общем, кто знает :)

А что мешало просто перевести УБ в ДБ?

Ну, у меня тут есть только предположения, хотя и много разных. Например, вызов std::bit_cast обозначает начало времени жизни соответствующего объекта явно, а обращение через неактивный член union - нет (особенно если обращение происходит посредством ссылок/указателей через третьи руки). std::bit_cast осуществляет минимальный набор проверок - что размеры типов совпадают и что оба типа являются "тривиально копируемыми", т.е. по факту могут быть скопированы через вызов memcpy(), иначе код не скомпилируется. Например, нельзя просто взять и написать такое:

char c = 0;
int i = std::bit_cast<int>(c);

union не выполняет никаких проверок вообще, потому что у него другая задача, а именно - просто хранить несколько объектов разных типов в одном storage, но не "одновременно". Ему плевать, если ты напишешь так:

union {
    char c;
    int i;
} u;

u.c = 0;
return u.i;

Можно и еще придумать причины.

bit_cast использует memcpy для объекта на стеке в своей реализации. При этом memcpy во многих реализациях берёт указатель от объекта и делает так *(char*)dst=*(const char*)scr;такие вещи всё ещё не нарушают стандарт? Могу ли я в таком случае заменить bit_cast на операцию присвоения типов через указатель, максимум обезопасив себя проверкой типа указателя на trivialy_copyable?

Это всё ещё не избавит меня от проблемы, что компилятор не будет знать о том что хранит union и UB не обойдём, но можно будет создавать временные прокси-объекты обычным выражением с указателем на this.

Нет, не нарушают, алиасинг к char* разрешён стандартом. Но алиасить можно не к любым типам. Поэтому в общем случае заменить bit_cast на алиасинг через указатель нельзя.

Вообще забавно как из такой очевидной проблемы растут неочевидные решения. Комитет С++ хочет создать абстрактный язык который оперировал объектами, чтоб можно было поменять рантайм даже на какой-нибудь CLR и всё работало. В этом случае работа с union кажется специфичной ибо окружение знает что в типе хранится "х" но не "у", как в таком случае гарантировать выполнение кода? Но эти же ограничения полный бред для низкоуровневых абстракций ибо "не важно что, там байты, дайте мне писать память", аоаоао. И это нельзя ничем решить нормально, не нарушая стандарт.

Ужас(((

Копался в анналах своего сознания и вспомнил за сужающие преобразования к void*. Что на этот счёт говорит стандарт?

Как я помню void* разрешает кастовать к нему и из него абсолютно любой cv-совместимый тип. Что так же используется в функции memcpy(void*,const void*,size_t). В таком случае если я приведу тип Т к void а потом приведу его к U будет ли это считаться UB? Я знаю что подобные шаманства используются с функциями, но там другие правила каста, так что немного не то...

Я вот ссылку давал чуть выше, если страничку по той ссылке прокрутить чуть вверх, там есть ответ на ваш вопрос:

5) Any object pointer type T1* can be converted to another object pointer type cv T2*. This is exactly equivalent to static_cast<cv T2*>(static_cast<cv void*>(expression)) (which implies that if T2's alignment requirement is not stricter than T1's, the value of the pointer does not change and conversion of the resulting pointer back to its original type yields the original value). In any case, the resulting pointer may only be dereferenced safely if allowed by the type aliasing rules (see below).

Мда, заболел как-то, голова совсем не варит, сорян. +На досуге перевариваю стандарт, относительно недавно начал.

Тогда получается использование сырых указателей, даже из всяких системных либ это UB? И единственный вариант использовать их это либо кастить результат в char* либо использовать placement new? Просто как в таком случае работает оператор new, который просто возвращает void*, если он самостоятельно приводит типы...? Ммм, читать....

Грустно однако. Ладно, и как в таком случае компилятор может гарантировать хранящийся тип в функциях на границе библиотек, при возвращении указателя на тот же union? Ведь ограничения на использования union исходят из того, что компилятор знает какой инициализированный тип может хранить union и не допускать использования другого, но тут эти гарантии нарушаются, так как компилятор не имеет информации за это, и тогда всё снова "зависит от реализации"? Или в таком случае запрещают использовать union на границах библиотек?

через границу либы я бы вообще не советовал протаскивать чтото сложнее plain array, бывали проблемы даже если собраны разными минорным версиями одного компилятора (в частности gcc). Ничего удобнее и надежнее C API для плюсовых библиотек люди пока не придумали кмк

И единственный вариант использовать их это либо кастить результат в char* либо использовать placement new?

Ну типа того, да. В C++23 ещё добавили std::start_lifetime_as.

Просто как в таком случае работает оператор new, который просто возвращает void*, если он самостоятельно приводит типы...?

operator new возвращает не void*, а строго указанный тип. Как это делается - внутреннее дело компилятора. Подразумевается, что компилятор позаботится о выделении корректно выравненного куска памяти нужного размера, вызовет конструкторы, сделает свой внутренний bookkeeping типа отслеживания pointer provenance, и так далее, это все детали реализации.

Ладно, и как в таком случае компилятор может гарантировать хранящийся тип в функциях на границе библиотек, при возвращении указателя на тот же union?

Никак не может. Он просто имеет право выполнять некие оптимизации (сейчас или в будущем), рассчитывая на то, что вы не будете делать ничего из того, что приводит к UB.

Наконец-то я за компом...

operator new был введён как расширение и долгое время шло заголовком new.h, в котором были опеределены все операторы с new, которые имели сигнатуру типа void *
operator new(std::size_t)
Позже его стали автоматически включать в проекты в рамках stdc++. Его так же можно переопределить и оно имеет свои реализации у соответсвующих компиляторах. Где-то оно дёргает вызовы шинды и вбрасывает исключения когда прям всё плохо, кто-то просто в цикле крутит malloc в надежде что вот ща то оно вернёт нужный указатель. Но суть что возвращает оно именно голый void*. И для меня, проблема в том, что компилятор сам занимается инициализацией и кастом этого указателя в нужный тип, что немного не сходится с дрочением типобезопасности. Почему это всё допускается тут? Только из-за того что это, формально, указатель на неинициализированный тип и внутри компилятора оно делает всё тот же placement new?
+Компилятор не должен содержать кода внутри себя, это библиотека, и единственное что ему дают делать это заниматься оптимизациями на основе выражений с new. Разве не так?

Насколько я знаю, идея типобезопасности идёт первостепенно внутри комитета. Использованиее union при специфичном рантайме, таком как java/.NET ведёт за собой большие проблемы. Но так как большая часть людей пишет на "низкоуровневом С++" это опускает внутренние проверки union и формирует из него то что и формирует - кусок памяти который можно представить по разному, что не типобезопасно и порецается комитетом. А используя некоторые махинации с TU, мы можем на горбу вертеть UB формально его не нарушать, но нарушать...слово забыл... Короче миграция на другой рантайм всё поломает, о чём и печётся комитет.

Это всё калдунство, а калдовать плоха. Но вот с new непонятнки какие-то.

Да, это я неправильно выразился. Есть operator new(), который просто выделяет память, но лайфтайм объекта это не начинает. А есть new expression, который может использовать operator new (а может и нет, например placement new), и вот это то что превращает void* в T*, в частности, вызывает конструкторы нопремер.

+Компилятор не должен содержать кода внутри себя, это библиотека, и единственное что ему дают делать это заниматься оптимизациями на основе выражений с new. Разве не так?

Не могу сказать, как конкретно компилятор работает с new expression, но думаю определённая кодогенерация тут должна быть, кто-то же должен вставлять код для вызова тех же конструкторов.

windows.h построен на UB, получается его нельзя использовать в С++ коде?

В windows.h есть довольно много типов, такие как LARGE_INTEGER. LARGE_INTEGER подразумевает что ты работаешь с 64битным числом через пару 32битных, и так же можешь как с одним 64 битным, при поддержке компилятором. Описан через union. ***Раньше не все умели в 64битные числа. Используется обычно для работы с точным временем.

Ну во-первых надо иметь в виду, что в C type punning через union разрешён, в том числе потому что в C нет концепции времен жизни как таковых, в отличие от C++. Поэтому если windows.h используется в C коде (или в модуле на C, линкуемом к C++ коду), то это не UB. А в C++ да, это UB, но тут MS видимо рассчитывает на то, что их компилятор будет делать все как надо, потому что они его контролируют, а то что это не переносимо - ну так это в любом случае Windows-only заголовок.

Забавно то, что почти все либы работают на UB. Многие библиотеки работающие с графикой используют union для доступа к членам. Даже могучий glm, который используется почти во всех OpenGL проектах использует техники как наш автор, при том они даже макросами варнинги в этих местах отключают

Получается ли тогда, что мир прогнил и любое дуновение и весь софт взорвётся?

А когдато было по другому? Условный Есс пропустил такт и сбойнул бит в ячейке, которая хранила принтер. Как себя поведет прога?

"Swizzle в vec2/vec3/vec4 внутри C++ как в OpenGL"

Чем кодогенерация inline методов с возвратом векторов из соответсвующих полей не подошла?

Что-то типа такого:

class vec2 {
public:
  int x;
  int y;
  // ctor, dtor, ...
  inline vec2 xx() { return vec2(x, x); }
  inline vec2 yy() { return vec2(y, y); }
  // ...
}

Не знаком с плюсами даже близко, так что поправьте если где-то ошибся

Это был как один из вариантов, если не получится с юнион, но хотелось все же нативный "шейдерный" синтаксис получить

Так и не понял, как планировалось использовать union? Для свиззлинга ведь нужны различные места памяти в исходном объекте, а юнион такого не может делать. В Си ещё хоть какой-то type punning был, а в плюсах доступ к неактивному члену это точно UB, да ещё и делает что-то не то.

Максимум, который мне представляется юнионом, это

// C
typedef union {
  struct {
    int x;
    int y;
  };
  int data[2];
} vec2;

vec2 a;
a.x;
a.y;
a.data[0];
a.data[1];

Неужели в C++ с ними придумали ещё более опасные для созерцания вещи?

Так положите рядом ещё одну структуру, которая называется yx и перепишите ей оператор присвоения и приведения. Собственно это и будет решением для свизлинга

Фраза непонятна:

Встали вопросы чтобы добиться такого же синтаксического и семантического поведения

спасибо, поправил

просто рабочая реализация (https://godbolt.org/z/ff55c4YnP)

Почему-то прямо с порога некорректный ответ (clang-trunk -O0):

veca{0.000000, 1.000000}
vecb{1076475645313255735296.000000, 1.000000}
vecc{1.000000, 2.000000, 3.000000}
vecd{1.000000, 0.000000, 3.000000}

На -O1 и выше всё норм. UB таки стреляет?

Виноват, какуюто старую ссылку подпихнул (спасибо, поправил)
https://godbolt.org/z/Pnq9654bW

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории