Иногда в С++ не хватает каких-то фич, которые есть в других языках. Мне, например, не хватает preperties из C#:
В них можно описать функции set/get, при этом работать с ними через поле класса, как с переменной. Очень удобно, например, когда после выставления значения нужно так же сделать какую-то работу. Например, при установке трансформации в акторе, сразу же обновить его визуальную составляющую:
class Actor
{
public Matrix4x4 transform
{
set { _transform = value; UpdateVisual(); }
get { return _transform; }
}
private UpdateVisual() { ... }
}
...
actor.transform = left.transform * righ.transform * up.transform;
В С++ это приходится делать через функции, что может быть громоздкои и менее читаемо:
actor.SetTransform(left.GetTransform() * right.GetTransform() * up.GetTransform());
Под катом я расскажу как можно в С++ обернуть set/get в переменную с минимальным размером 1 байт
Наивная реализация
Итак, для начала нам нужны собственно функции Set/Get(). Повторить синтаксис C#, к сожалению, не возможно, поэтому функции Set/Get() нужно писать прямо в теле класса. В целом это даже выглядит логично и лаконично.
Допустим, у нас есть поле transform, и мы хотим сделать для него property. Напишем для него SetTransform()/GetTransform()
struct Actor
{
// Matrix4x4 transform; // наша будущая property
void SetTransform(const Matrix4x4& mtx) { _transform = mtx; UpdateVisual(); }
Matrix4x4 GetTransform() const { return _transform; }
private:
Matrix4x4 _transform;
};
Теперь мы хотим чтобы при работе с публичным полем transform вызывались SetTransform/GetTransform() при чтении и записи значения.
Для этого заведем шаблонный класс Property<> с пачкой методов operator, чтобы имитировать поведение установки и чтения значения, а так же математических операций.
Чтобы передать setter/getter, можем воспользоваться обычными std::function<>. Да, это ресурсозатратный способ, но для понимания базовой концепции воспольуемся им:
template<typename TYPE>
class Property
{
std::function<void(TYPE)> _setter; // Функция установки значения
std::function<TYPE()> _getter; // Функция получения значения
public:
using valueType = TYPE;
// Конструктор из setter и getter функций
Property(std::function<void(TYPE)> setter, std::function<TYPE()> getter):_setter(setter), _getter(getter) {}
// Получение значения
valueType Get() const { return _getter(); }
// Установка значения
void Set(const valueType& value) { _setter(const_cast<valueType&>(value)); }
// Оператор приведения к значению, для получения значения
operator valueType() const { return _getter(); }
// Оператор присваивания, для установки значения
Property& operator=(const valueType& value) { _setter(const_cast<valueType&>(value)); return *this; }
// Оператор присваивания, для установки значения из другого свойства
Property& operator=(const Property& value) { _setter(value.Get()); return *this; }
// Оператор равенства, для сравнения значений
template<typename V, typename X = typename std::enable_if<std::is_same<V, valueType>::value && SupportsEqualOperator<valueType>::value>::type>
bool operator==(const V& value) const { return _getter() == value; }
// Оператор неравенства, для сравнения значений
template<typename V, typename X = typename std::enable_if<std::is_same<V, valueType>::value && SupportsEqualOperator<valueType>::value>::type>
bool operator!=(const V& value) const { return _getter() != value; }
// Оператор сложения, для сложения значений
template<typename T, typename X = typename std::enable_if<SupportsPlus<valueType>::value && std::is_same<T, valueType>::value>::type>
valueType operator+(const T& value) { return _getter() + value; }
// Оператор вычитания, для вычитания значений
template<typename T, typename X = typename std::enable_if<SupportsMinus<valueType>::value && std::is_same<T, valueType>::value>::type>
valueType operator-(const T& value) { return _getter() - value; }
// Оператор деления, для деления значений
template<typename T, typename X = typename std::enable_if<SupportsDivide<valueType>::value && std::is_same<T, valueType>::value>::type>
valueType operator/(const T& value) { return _getter() / value; }
// Оператор умножения, для умножения значений
template<typename T, typename X = typename std::enable_if<SupportsMultiply<valueType>::value && std::is_same<T, valueType>::value>::type>
valueType operator*(const T& value) { return _getter() * value; }
// Оператор сложения с присваиванием, для сложения значений с присваиванием
template<typename T, typename X = typename std::enable_if<SupportsPlus<valueType>::value && std::is_same<T, valueType>::type>
Property& operator+=(const T& value) { _setter(_getter() + value); return *this; }
// Оператор вычитания с присваиванием, для вычитания значений с присваиванием
template<typename T, typename X = typename std::enable_if<SupportsMinus<valueType>::value && std::is_same<T, valueType>::value>::type>
Property& operator-=(const T& value) { _setter(_getter() - value); return *this; }
// Оператор деления с присваиванием, для деления значений с присваиванием
template<typename T, typename X = typename std::enable_if<SupportsDivide<valueType>::value && std::is_same<T, valueType>::value>::type>
Property& operator/=(const T& value) { _setter(_getter() / value); return *this; }
// Оператор умножения с присваиванием, для умножения значений с присваиванием
template<typename T, typename X = typename std::enable_if<SupportsMultiply<valueType>::value && std::is_same<T, valueType>::value>::type>
Property& operator*=(const T& value) { _setter(_getter() * value); return *this; }
};
Здесь стоит обратить внимание на шаблонную магию в операторах математических операций. Она нужна чтобы не определять методы для типов, не поддерживающих конкретные математические операции, иначе будет ошибка компиляции.
template<typename T, typename X = typename std::enable_if<SupportsMultiply<valueType>::value && std::is_same<T, valueType>::value>::type>
В первой части мы указываем шаблон предполагаемой переменной, с которой хотим взаимодействовать, т.к. он может отличаться от типа property
-
Вторая часть - магия type traits, которая говорит возможна ли эта математическая переменная между TYPE и T.
std::enable_if через SFINAE "отключает" шаблонную функцию из класса, если не удовлетворяется условие
SupportsMultiply. В данном случае - если между типами невозможна математическая операция, то шаблонный метод перегрузки математической операции не генерируется
Реализация SupportsMultiply
template<class T, class = void_t<>>
struct SupportsMinus : std::false_type {};
template<class T>
struct SupportsMinus<T, void_t<decltype(std::declval<T>() * std::declval<T>())>> : std::true_type {};Вернемся к нашему классу Actor, попробуем в нем определить property:
struct Actor
{
Property<Matrix4x4> transform = Property<Matrix4x4>([this](Matrix4x4 x) { SetTransform(x); }, [this]() { return GetTransform(); }); // А вот и наша property
void SetTransform(const Matrix4x4& mtx) { _transform = mtx; UpdateVisual(); }
Matrix4x4 GetTransform() const { return _transform; }
private:
Matrix4x4 _transform;
};
Немножко громоздко, но уже выполняет свою функцию: transform выглядит как переменная, с ней можно работать в математических операциях (которые позволяет тип Matrix4x4).
Но вещь получается довольно тяжелая из-за использования std::function<>. Во-первых, у него немаленький размер (х2 в нашем случае), во-вторых куча накладных расходов на инициализацию и вызов. Очевидно, от std::function<> надо избавляться.
Оптимизированное решение
Так как мы работаем с функциями класса, то мы можем сократить накладные расходы хранив указатель на объект (this) и пару указателей на функции setter/getter.
template<typename CLASS, typename TYPE>
class Property
{
CLASS* _owner; // Указатель на объект
TYPE(CLASS::*_getter)(); // Указатель на функцию получения значения
void(CLASS::*_setter)(const TYPE&); // Указатель на функцию установки значения
public:
using valueType = TYPE;
// Конструктор из setter и getter функций
Property(CLASS* owner, TYPE(CLASS::*getter)(), void(CLASS::*setter)(const TYPE&)):_owner(owner), _getter(getter), _setter(setter) {}
// Получение значения
valueType Get() const { return (_owner->*_getter)(); }
// Установка значения
void Set(const valueType& value) { (_owner->*_setter)(const_cast<valueType&>(value)); }
// Оператор приведения к значению, для получения значения
operator valueType() const { return (_owner->*_getter)(); }
// Оператор присваивания, для установки значения
Property& operator=(const valueType& value) { (_owner->*_setter)(const_cast<valueType&>(value)); return *this; }
... остальная реализация ...
};
Уже лучше, но все еще храним целых три указателя. Если таких property в классе много, а самих объектов тысячи (напр. акторы в игровом движке), может быть критично по памяти.
Оптимизируем еще
disclaimer: спорное решние, но учитывая особенности может быть как неплохой компромисс
Чтобы не хранить указатели на объект и пару setter/getter, их можно вычислять почти полностью в compile time.
Чтобы получить указатель на объект, владеющий property, можно использовать offsetof(class, field) и вычитать его из this от roperty. Идея такая: раз мы знаем указатель на самого себя, и в compile time знаем оффсет от начала класса до property, можем вычислить адрес владеющего property объекта.
_propertiesClassType * GetThis() const
{
return reinterpret_cast<_propertiesClassType*>(
const_cast<std::byte*>(reinterpret_cast<const std::byte*>(this)) - offsetof(_propertiesClassType, NAME));
}
А для хранения указателей на setter/getter можно воспользоваться специализацией класса property для конкретной пары setter/getter. Чтобы корректно с ней работать так или иначе нужно заворачивать это все в макрос.
У себя в проекте я выбрал подход, где в макросе определяется целый класс, со специализацией:
#define PROPERTIES(CLASSNAME) \
typedef CLASSNAME _propertiesClassType
#define PROPERTY(TYPE, NAME, SETTER, GETTER) \
class NAME##_PROPERTY \
{ \
_propertiesClassType* GetThis() const \
{ \
return reinterpret_cast<_propertiesClassType*>( \
const_cast<std::byte*>(reinterpret_cast<const std::byte*>(this)) - offsetof(_propertiesClassType, NAME)); \
} \
\
public: \
typedef TYPE valueType; \
\
NAME##_PROPERTY() {} \
\
operator valueType() const { return GetThis()->GETTER(); } \
NAME##_PROPERTY& operator=(const valueType& value) { GetThis()->SETTER(const_cast<valueType&>(value)); return *this; } \
\
NAME##_PROPERTY& operator=(const NAME##_PROPERTY& value) { GetThis()->SETTER(value.Get()); return *this; } \
\
template<typename vt, typename X = typename std::enable_if<std::is_same<vt, valueType>::value && SupportsEqualOperator<valueType>::value>::type> \
bool operator==(const vt& value) const { return Math::Equals(GetThis()->GETTER(), value); } \
\
template<typename vt, typename X = typename std::enable_if<std::is_same<vt, valueType>::value && SupportsEqualOperator<valueType>::value>::type> \
bool operator!=(const vt& value) const { return !Math::Equals(GetThis()->GETTER(), value); } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsPlus<valueType>::value && std::is_same<T, valueType>::value>::type> \
valueType operator+(const T& value) { return GetThis()->GETTER() + value; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsMinus<valueType>::value && std::is_same<T, valueType>::value>::type> \
valueType operator-(const T& value) { return GetThis()->GETTER() - value; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsDivide<valueType>::value && std::is_same<T, valueType>::value>::type> \
valueType operator/(const T& value) { return GetThis()->GETTER() / value; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsMultiply<valueType>::value && std::is_same<T, valueType>::value>::type> \
valueType operator*(const T& value) { return GetThis()->GETTER() * value; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsPlus<valueType>::value && std::is_same<T, valueType>::value>::type> \
NAME##_PROPERTY& operator+=(const T& value) { auto _this = GetThis(); _this->SETTER(_this->GETTER() + value); return *this; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsMinus<valueType>::value && std::is_same<T, valueType>::value>::type> \
NAME##_PROPERTY& operator-=(const T& value) { auto _this = GetThis(); _this->SETTER(_this->GETTER() - value); return *this; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsDivide<valueType>::value && std::is_same<T, valueType>::value>::type> \
NAME##_PROPERTY& operator/=(const T& value) { auto _this = GetThis(); _this->SETTER(_this->GETTER() / value); return *this; } \
\
template<typename T, typename X = typename std::enable_if<o2::SupportsMultiply<valueType>::value && std::is_same<T, valueType>::value>::type> \
NAME##_PROPERTY& operator*=(const T& value) { auto _this = GetThis(); _this->SETTER(_this->GETTER() * value); return *this; } \
\
valueType Get() const { return GetThis()->GETTER(); } \
void Set(const valueType& value) { GetThis()->SETTER(const_cast<valueType&>(value)); } \
\
PropertyValueProxy<valueType, NAME##_PROPERTY> GetValueProxy() { return PropertyValueProxy<valueType, NAME##_PROPERTY>(this); } \
\
bool IsProperty() const { return true; } \
}; \
\
NAME##_PROPERTY NAME;
Но есть вариант с менее монструозным макросом, с вынесением функционала в шаблонный класс: godbolt example (спасибо ИИ за генерацию)
Pross & Cons
Самый большой плюс - это отсутствие полей внутри класса property. В C++ нулевого размера типа не может быть, минимум 1 байт. Это как раз таки удобно, ведь на сам property иногда нужно взять указатель.
Но есть и минус - на каждую property генерируется микро-класс, специализирующийся под конкретный класс и пару setter/getter. От этого "пухнет" бинарник, а в отладке прилично накладных ресурсов.
Однако даже в отладке эти накладные затраты почти незаметны, а современные компиляторы отлично оптимизируют бинарник, ведь эти property-классы отлично оптимизируются.
Хорош подход или плох, имхо, дело вкуса и потребностей. В своем проекте я использую его, учитывая его минусы. Лично мне нравится более лаконичный синтаксис, а так же я извлекаю некие профиты через рефлексию.
А было бы прикольно если бы фича попала в стандарт С++ с нормальным синтаксисом и оптимизациями...
Подписывайтесь на мой уютный неформальный telegram-канал про разработки игрового движка на С++: https://t.me/o2engine
Комментарии (22)

Myz17
14.10.2025 17:43Всего-то надо было идею MS в стандарт затащить property (C++) | Microsoft Learn

SpiderEkb
14.10.2025 17:43Вообще-то, идея, насколько помню, была от Borland. Появилась в Delphy, потом оттуда пришла в C++ Builder.
private: int readX; void writeX(int Value); double GetZ(); float cellValue(int row, int col); void setCellValue(int row, int col, float Value); int GetCoordinate(int Index); void SetCoordinate(int Index, int Value);// Standard, common property declaration, reading from a field and writing via a method: __property int X = {read = readX, write = writeX}; // A read-only property (note the absence of the write specifier.) __property double Z = {read = GetZ}; // An indexed property - note two indices in use: __property float Cells[int row][int col] = {read = cellValue, write = setCellValue}; // Redeclaring a property declared in an ancestor class (used for redeclaring in a wider visibility scope, such as bringing an ancestor protected property to [[public]] or [[__published]] scope): __property Foo; // Wrapping an indexed method into a simple property: __property int Left = { index = 0, read = GetCoordinate, write = SetCoordinate }; // Another indexed method wrapped to a simple property, this time with specifiers used for streaming: __property int Top = { read = GetCoordinate, write = SetCoordinate, index = 1, stored = true, default = 5 };

Einherjar
14.10.2025 17:43Слишком просто, какой же это C++ без нагромождения шаблонов в макросах? Стандарт должен усложнять язык, а не упрощать его.

tenzink
14.10.2025 17:43IMHO, прикольно для своей песочницы, но строго противопоказанно для проекта с числом разработчиков больше одного и хоть сколько заметным временем жизни.
В статье сеттер вызывает функцию UpdateVisual, которая скорее всего заметно тяжелее присваивания матриц. И вот тут начинаются проблемы. Программисты на C++ от конструкции `a.transform = some_matrix` обычно не ждут подвоха в отличии от
a.SetTransform(some_matrix). Совершенно нормально написать такой код, возможно даже шаблонныйfor(const auto& tr : transforms) { a.transform *= tr; }И получить premature pessimization на ровном месте

uvic
14.10.2025 17:43Да. На первой работе лет 30 назад столкнулся с property на Дельфи. Сам я Дельфи знал очень плохо ( типа это почти Паскаль, который учили в школе )...
И программист который писал до меня проект то-же отличался оригинальным мышлением.
В реализацию проперти засунул отправку сообщения по модему.
Я искал причину тормозов две недели. Только по ассемблерному листингу понял, что простое обращение к члену класса вызывает что-то подозрительное ;))Так я познакомился с property в Дельфи ;)))

Jijiki
14.10.2025 17:43ну вы клоните к тому что должна быть функция модификатор или класс, если я правильно понял, тогда просто обходим коллекцию обьектов по выбранному полю
например функция, которая только рисует используя либо выделенные нужные поля в структуры, другая считает только матрицы грубо говоря или обновляет кости
их можно либо обьединить в большую структуру или оставить флай обджектами, потомучто у С++ есть автоудаление
Скрытый текст
enum OBJTYPES { STATIC, DINAMIC, TERRAIN, SKELETALANIMATION, LIGHT, }; struct PASSES{ std::vector<Shader *> shader; }; struct RenderObject{ Object3D *ptr; glm::vec3 *pos; GLuint *VAO; OBJTYPES type; size_t size; GLuint *textureID; }; template<typename T> struct SceneNode { T object; SceneNode* left; SceneNode* right; SceneNode(const T& obj) : object(obj), left(nullptr), right(nullptr) {} }; template<typename T> SceneNode<T>* insertNode(SceneNode<T>* root, const T& obj) { if (root == nullptr) return new SceneNode<T>(obj); // Вставляем по какому-то критерию, например, по size if (obj.size < root->object.size) root->left = insertNode(root->left, obj); else root->right = insertNode(root->right, obj); return root; } template<typename T> void renderTreePASS(const SceneNode<T>* root, Shader *shader) { if (root == nullptr) return; // Обработка левого поддерева renderTreePASS(root->left, shader); // Рендер текущего объекта const T& obj = root->object; if (obj.VAO && *obj.VAO) glBindVertexArray(*obj.VAO); else return; // или обработка ошибки if (obj.textureID && *obj.textureID) glBindTexture(GL_TEXTURE_2D, *obj.textureID); glm::mat4 model = glm::mat4(1.f); if (obj.type == OBJTYPES::TERRAIN && obj.pos) { model = glm::translate(glm::mat4(1.f), *obj.pos); } else if (obj.type == OBJTYPES::DINAMIC && obj.ptr) { model = glm::translate(glm::mat4(1.f), obj.ptr->position); } shader->setMat4("model", model); // Предполагается, что шейдер уже активен glDrawElements(GL_TRIANGLES, obj.size, GL_UNSIGNED_INT, 0); glBindVertexArray(0); // Обработка правого поддерева renderTreePASS(root->right, shader); } template<typename T> void renderTree(const SceneNode<T>* root,PASSES* p,int i) { renderTreePASS(root,p->shader[i]); }PASS это и есть поле проперти оно определит какие будут проходы
большие поля - которые флай обджекты тоже, можно линковать указателями тогда появляется образно успех такого подхода, у нас в структуре на указателе нужные поля
у меня правда в примере пока статик и статичные-динамики, скелетку пока не добавил )
а в скелетке если 150 костей, как-то надо посчтать все анимации, кинуть предпосчитанные ключи матриц(бейкинг всех анимаций модельки), чтобы счет был только в ГПУ на весах по фрейму, чтобы можно было тоже как флай обджекты рендерить если есть повторные модельки

segment
14.10.2025 17:43Но ведь тогда скрывается сущность, и снаружи становится непонятно - делает ли что-то обращение к свойству или нет. Функция или метод явно говорят об этом.

nickolaym
14.10.2025 17:43По поводу обмазывания операторов проверками.
1) std::enable_if - это уже немножко древность. Есть же requires.
2) слишком жёсткие ограничения на ровном месте. Достаточно проверить существование оператора для исходных типов, а не требовать, чтобы тип совпал
auto operator == (const auto& v) const requires { this->_getter() == v; } { return this->_getter() == v; }(для старого доброго сфинае это же самое на decltype)
template<class V> auto operator == (const V& v) const -> decltype(this->_getter() == v) { return this->_getter() == v; }

Jijiki
14.10.2025 17:43кстати structure Actor можно избавить от функций вынести их чтобы структура полегше была, и просто хранила трансформу, наверное
тогда
struct sideProp{ bool ready; float coef; vec3 cf; }; struct Actor{ sideProp *effect;//на всякий случай это адресс переменной - тоесть это хендлер mat4 transform; }; updateTransform(Actor *a,vec3 t0,vec3 r,vec3 s,float t1,sideProp *effect); updateTransform(Actor *a,mat4 t0,float t1,sideProp *effect);

73xc
14.10.2025 17:43Спасибо за статью. Было бы интересно как возможное продолжение рассмотреть реализацию автоматических свойств (без backing field в коде класса, использующем Property), readonly свойств (достаточно ли и можно ли убрать getter?) и модификаторов свойств из C# (required, init setter, применение отличных модификаторов доступа к методам-аксессорам).

ReadOnlySadUser
14.10.2025 17:43Но это даже близко не C#-like свойства. Смысл был бы, если бы финальный синтаксис получится бы каким-то таким
class Example { Property<int> x = { .get = &Example::get_x, .set = &Example::get_y }; };Ну, понятное дело, что ещё надо будет где-то прикопать this и всё такое и возможно для этого можно было бы немного макросов добавить.
А финальный результат из статьи - это просто очередная монструозная конструкция из макросов, такого придумали уже все подряд)
P.S.
Уже 26-й год скоро) Можно вместое enable_if использовать концепты

domix32
14.10.2025 17:43Правда не очень понятно зачем вам такая локализация приватных данных - с точки зерния оптимальности это не очень выгодно.
using HealthProp = Property<int, SetHealth, GetHealth>;И держали бы приватные данные прям там же в пропери. Глядишь через пару шагов ешё и в ECS какой-нибудь превратилось бы.
Ну и да, у вас же 20 стандарт включен, используйте концепты вместо enable_if

ReadOnlySadUser
14.10.2025 17:43Уже лучше, но все еще храним целых три указателя. Если таких property в классе много, а самих объектов тысячи (напр. акторы в игровом движке), может быть критично по памяти.
Возьём 100 свойств на объект и 10000 акторов. Указатель на self - 8 байт + два указателя на функции-члены (16+16 = 32 байта). 40 х 100 х 10000 = 35 MiB памяти. Сомнительная экономия.

anz Автор
14.10.2025 17:43для мобильных игр - это весьма прилично. Если поддерживать слабые девайсы с 1ГБ ОЗУ, то приложению по факту доступно всего около 600мб. В таком случае 35мб может оказаться критично

ReadOnlySadUser
14.10.2025 17:43Такие телефоны в жизни не потянут 10000 акторов на сцене) Там дай бог 1000-то уместится)

kovserg
14.10.2025 17:43Иногда в С++ не хватает каких-то фич, которые есть в других языках. Мне, например, не хватает preperties из C#
А нехватает для чего? Что бы можно было по имени обращаться с свойствам класса? Что бы можно было отслеживать измениния? Или просто привыкли и скучаете? Если не нравиться синтаксис, так кто мешает сначала все значения сложить в переменные выполнить выражение и положить результат обратно?
Я бы предложил сделать отдельный класс для доступа к полям класса и использовать его. Что то такого вида:
пример
prop.h
#include <stdarg.h> namespace PropNames { enum Op { Get,Set,GetConstRef,Swap,GetName,GetType,FindByName }; // Get can convert values // Set can convert values // GetConstRef only for reading, can't convert // Swap - can't convert exchange only same types enum { NoName=-1 }; enum Types { NoType=-1,TypeInt,TypeDouble }; template<class T>int type(T&); template<> int type(int&) { return TypeInt; } template<> int type(double&) { return TypeDouble; } const char* type_name(int type); int prop_def_op(int &v,const char* name,int op, int type,void* data,int size,int index); int prop_def_op(double &v,const char* name,int op, int type,void* data,int size,int index); } struct Prop_s { typedef int (*op_fn) (void* ctx,int name,int op,int type,void* data,int size,int index); void *ctx; op_fn op; }; struct Prop : Prop_s { Prop(op_fn op,void *ctx) { this->ctx=ctx; this->op=op; } template<class T>Prop& get(int name,T& value,int index=0) { int rc=op(ctx,name,PropNames::Get,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_op("get",name,index); return *this; } template<class T>Prop& set(int name,T value,int index=0) { int rc=op(ctx,name,PropNames::Set,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_op("set",name,index); return *this; } template<class T>Prop& swap(int name,T& value,int index=0) { int rc=op(ctx,name,PropNames::Swap,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_oprt("swap",name,index,PropNames::type(value)); return *this; } template<class T>const T& ref(int name,int index=0) { T *value=0; int rc=op(ctx,name,PropNames::GetConstRef,PropNames::type(*value), (void*)&value,sizeof(value),index); if (!value) throw_oprt("ref",name,index,PropNames::type(*(T*)0)); if (rc) throw_op("ref",name,index); return *value; } const char* name(int name) { const char* value="?"; op(ctx,name,PropNames::GetName,PropNames::NoType, (void*)&value,sizeof(value),0); return value; } int type(int name) { return op(ctx,name,PropNames::GetType,PropNames::NoType,0,0,0); } const char* type_name(int name) { return PropNames::type_name(type(name)); } int find(const char* name) { return op(ctx,PropNames::NoName,PropNames::FindByName, PropNames::NoType,(void*)name,-1,0); } template<class T>Prop& get(const char *name,T& value,int index=0) { int iname=find(name); if (iname==PropNames::NoName) throw_error("get(?'%s')",name); int rc=op(ctx,iname,PropNames::Get,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_op("get",name,index); return *this; } template<class T>Prop& set(const char *name,T value,int index=0) { int iname=find(name); if (iname==PropNames::NoName) throw_error("set(?'%s')",name); int rc=op(ctx,iname,PropNames::Set,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_op("set",name,index); return *this; } template<class T>Prop& swap(const char *name,T& value,int index=0) { int iname=find(name); if (iname==PropNames::NoName) throw_error("swap(?'%s')",name); int rc=op(ctx,iname,PropNames::Swap,PropNames::type(value), (void*)&value,sizeof(value),index); if (rc) throw_oprt("swap",name,index,PropNames::type(value)); return *this; } template<class T>const T& ref(const char *name,int index=0) { int iname=find(name); if (iname==PropNames::NoName) throw_error("ref(?'%s')",name); T *value=0; int rc=op(ctx,iname,PropNames::GetConstRef,PropNames::type(*value), (void*)&value,sizeof(value),index); if (!value) throw_oprt("ref",name,index,PropNames::type(*(T*)0)); if (rc) throw_op("ref",name,index); return *value; } void throw_error(const char* msg,...); void vthrow_error(const char* msg,va_list v); void throw_op(const char* op_name,int name,int index); void throw_op(const char* op_name,const char* name,int index); void throw_oprt(const char* op_name,int name,int index,int rtype); void throw_oprt(const char* op_name,const char *name,int index,int rtype); };prop.cpp
#include "prop.h" #include <stdio.h> #include <string.h> void Prop::vthrow_error(const char* msg,va_list v) { printf("ERROR: "); vprintf(msg,v); printf("\n"); throw this; } void Prop::throw_error(const char* msg,...) { va_list v; va_start(v,msg); vthrow_error(msg,v); va_end(v); } const char* PropNames::type_name(int type) { switch(type) { case NoType: return "NoType"; case TypeInt: return "int"; case TypeDouble: return "double"; } return "?"; } void Prop::throw_op(const char* op_name,int name,int index) { if (index) throw_error("%s(%d=%s[%d])",op_name,name,this->name(name),index); throw_error("%s(%d=%s)",op_name,name,this->name(name)); } void Prop::throw_op(const char* op_name,const char* name,int index) { if (index) throw_error("%s('%s'[%d])",op_name,name,index); throw_error("%s('%s')",op_name,name); } void Prop::throw_oprt(const char* op_name,int name,int index,int rtype) { int vtype=type(name); if (rtype!=vtype) { const char* rtype_name=PropNames::type_name(rtype); const char* vtype_name=PropNames::type_name(vtype); throw_error("%s(%d:'%s') %s<->%s ?", op_name,name,this->name(name),vtype_name,rtype_name); } if (index) throw_error("%s(%d=%s[%d])",op_name,name,this->name(name),index); throw_error("%s(%d=%s)",op_name,name,this->name(name)); } void Prop::throw_oprt(const char* op_name,const char *name,int index,int rtype) { int iname=find(name); int vtype=type(iname); if (rtype!=vtype) { const char* rtype_name=PropNames::type_name(rtype); const char* vtype_name=PropNames::type_name(vtype); throw_error("%s('%s') %s<->%s ?", op_name,name,vtype_name,rtype_name); } if (index) throw_error("%s('%s'[%d])",op_name,name,index); throw_error("%s('%s')",op_name,name); } int PropNames::prop_def_op(int &v,const char* name,int op, int type,void* data,int size,int index) { int ptype=PropNames::type(v); if (op==GetType) return ptype; if (op==GetName) { if (size<(int)sizeof(name)) return -1; memcpy(data,&name,sizeof(name)); return 0; } if (index!=0) return -1; if (op==Set && type==TypeDouble) { // convert double->int double vd; if (size<(int)sizeof(vd)) return -1; memcpy(&vd,data,sizeof(vd)); int vi=vd; if (vi!=vd) return -1; // unable to convert return prop_def_op(v,name,op,TypeInt,&vi,sizeof(vi),index); } if (type!=ptype) return -1; int sz=(int)sizeof(v); if (op==GetConstRef) sz=(int)sizeof(&v); if (size<sz) return -1; switch(op) { case Get: { memcpy(data,&v,sizeof(v)); } break; case Set: { memcpy(&v,data,sizeof(v)); } break; case GetConstRef: { void *pv=&v; memcpy(data,&pv,sizeof(pv)); } break; case Swap: { //memswp(data,&v,sizeof(v)); char t[sizeof(v)]; memcpy(&t,&v,sizeof(v)); memcpy(&v,data,sizeof(v)); memcpy(data,&t,sizeof(v)); } break; default: return -1; } return 0; } int PropNames::prop_def_op(double &v,const char* name,int op,int type,void* data,int size,int index) { int ptype=PropNames::type(v); if (op==GetType) return ptype; if (op==GetName) { if (size<(int)sizeof(name)) return -1; memcpy(data,&name,sizeof(name)); return 0; } if (index!=0) return -1; if (op==Set && type==TypeInt) { // convert int->double int vi; if (size<(int)sizeof(vi)) return -1; memcpy(&vi,data,sizeof(vi)); double vd=vi; return prop_def_op(v,name,op,TypeDouble,&vd,sizeof(vd),index); } if (type!=ptype) return -1; int sz=(int)sizeof(v); if (op==GetConstRef) sz=(int)sizeof(&v); if (size<sz) return -1; switch(op) { case Get: { memcpy(data,&v,sizeof(v)); } break; case Set: { memcpy(&v,data,sizeof(v)); } break; case GetConstRef: { void *pv=&v; memcpy(data,&pv,sizeof(pv)); } break; case Swap: { //memswp(data,&v,sizeof(v)); char t[sizeof(v)]; memcpy(&t,&v,sizeof(v)); memcpy(&v,data,sizeof(v)); memcpy(data,&t,sizeof(v)); } break; default: return -1; } return 0; }a.h
struct A { int x; double y; enum { X,Y }; Prop prop() { return Prop(prop_op_ref,this); } static A* my(void *ctx) { return (A*)ctx; } static int prop_op_ref(void* ctx,int name,int op, int type,void* data,int size,int index) { return my(ctx)->prop_op(name,op,type,data,size,index); } int prop_op(int name,int op,int type,void* data,int size,int index); };a.cpp
#include "prop.h" #include <string.h> int A::prop_op(int name,int op,int type,void* data,int size,int index) { using namespace PropNames; if (op==FindByName) { const char* req=(const char*)data; if (strcmp(req,"X")==0) return X; if (strcmp(req,"Y")==0) return Y; return -1; } switch(name) { case X: return prop_def_op(x,"X",op,type,data,size,index); case Y: return prop_def_op(y,"Y",op,type,data,size,index); } return -1; }example.cpp
#include "a.h" #include <stdio.h> int main(int argc,char** argv) { A a; try { Prop pa=a.prop(); pa.set("X",1e3).set("Y",2); const int& rx=pa.ref<int>(A::X); const double& ry=pa.ref<double>(A::Y); int x; double y; pa.get(A::X,x).get(A::Y,y); printf("X=%d a.x=%d &x=%d\n",x,a.x,rx); printf("Y=%.2f a.y=%.2f &y=%.2f\n",y,a.y,ry); x=x+y; pa.swap("X",x); printf("x=%d a.x=%d\n",x,a.x); } catch(Prop*) { printf("ups\n"); } return 0; }А когда нужно вычислять выражения сначала получить все поля по константным ссылкаи посчитать и положить обратно или можно забирать тяжелые данные с помощью операции обмена данных.
struct A { int x; double y; enum { X,Y }; // имена полей Prop prop(); // получение доступа к полям ... }; int main(int argc,char** argv) { A a; try { Prop pa=a.prop(); pa.set("X",1e3).set("Y",2); const int& rx=pa.ref<int>(A::X); const double& ry=pa.ref<double>(A::Y); int x; double y; pa.get(A::X,x).get(A::Y,y); printf("X=%d a.x=%d &x=%d\n",x,a.x,rx); printf("Y=%.2f a.y=%.2f &y=%.2f\n",y,a.y,ry); x=x+y; pa.swap("X",x); printf("x=%d a.x=%d\n",x,a.x); } catch(Prop*) { printf("problem\n"); } return 0; }
anz Автор
14.10.2025 17:43А нехватает для чего?
Так вот в начае же как раз, чтобы иметь нормальный синтаксис математических операций
actor.transform = left.transformrigh.transformup.transform;

UdUser
14.10.2025 17:43https://godbolt.org/z/e1h16r3Ye
буквально одной строкой можно доработать до 0 байт, если уже используется С++17static_assert(sizeof(Actor) == sizeof(int));
anz Автор
14.10.2025 17:43интересно, спасибо ) но мне как раз хоть какой-то размер нужен, т.к. в через рефлексию могу ссылаться на property по указателю

UdUser
14.10.2025 17:43Да должно работать и с рефлексией, уникальный адрес нужен только, если у вас экзотическая реализация с проверкой полей на уникальность по их адресу. Но я ошибся, нужен всё-таки C++20
OldFisher
Со временем я пришёл к убеждению, что когда кажется, что нужны property, надо первым делом проверить, нет ли грязи в архитектуре. Просто потому что они тянут "кишочки" (т.е. детали реализации) наружу, в интерфейс. Хотя формально это оборачивается в "защитные" методы-прокладки, семантически происходит как раз это. А значит, нарушаются уровни абстракции и надо посмотреть, что перекособочено и поправить, пока не стало поздно и дорого.