Comments 23
Со временем я пришёл к убеждению, что когда кажется, что нужны property, надо первым делом проверить, нет ли грязи в архитектуре. Просто потому что они тянут "кишочки" (т.е. детали реализации) наружу, в интерфейс. Хотя формально это оборачивается в "защитные" методы-прокладки, семантически происходит как раз это. А значит, нарушаются уровни абстракции и надо посмотреть, что перекособочено и поправить, пока не стало поздно и дорого.
Всего-то надо было идею MS в стандарт затащить property (C++) | Microsoft Learn
Вообще-то, идея, насколько помню, была от 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 };Слишком просто, какой же это C++ без нагромождения шаблонов в макросах? Стандарт должен усложнять язык, а не упрощать его.
IMHO, прикольно для своей песочницы, но строго противопоказанно для проекта с числом разработчиков больше одного и хоть сколько заметным временем жизни.
В статье сеттер вызывает функцию UpdateVisual, которая скорее всего заметно тяжелее присваивания матриц. И вот тут начинаются проблемы. Программисты на C++ от конструкции `a.transform = some_matrix` обычно не ждут подвоха в отличии от a.SetTransform(some_matrix). Совершенно нормально написать такой код, возможно даже шаблонный
for(const auto& tr : transforms) {
a.transform *= tr;
}И получить premature pessimization на ровном месте
Да. На первой работе лет 30 назад столкнулся с property на Дельфи. Сам я Дельфи знал очень плохо ( типа это почти Паскаль, который учили в школе )...
И программист который писал до меня проект то-же отличался оригинальным мышлением.
В реализацию проперти засунул отправку сообщения по модему.
Я искал причину тормозов две недели. Только по ассемблерному листингу понял, что простое обращение к члену класса вызывает что-то подозрительное ;))
Так я познакомился с property в Дельфи ;)))
ну вы клоните к тому что должна быть функция модификатор или класс, если я правильно понял, тогда просто обходим коллекцию обьектов по выбранному полю
например функция, которая только рисует используя либо выделенные нужные поля в структуры, другая считает только матрицы грубо говоря или обновляет кости
их можно либо обьединить в большую структуру или оставить флай обджектами, потомучто у С++ есть автоудаление
Скрытый текст
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 костей, как-то надо посчтать все анимации, кинуть предпосчитанные ключи матриц(бейкинг всех анимаций модельки), чтобы счет был только в ГПУ на весах по фрейму, чтобы можно было тоже как флай обджекты рендерить если есть повторные модельки
Но ведь тогда скрывается сущность, и снаружи становится непонятно - делает ли что-то обращение к свойству или нет. Функция или метод явно говорят об этом.
По поводу обмазывания операторов проверками.
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; }кстати 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);
Спасибо за статью. Было бы интересно как возможное продолжение рассмотреть реализацию автоматических свойств (без backing field в коде класса, использующем Property), readonly свойств (достаточно ли и можно ли убрать getter?) и модификаторов свойств из C# (required, init setter, применение отличных модификаторов доступа к методам-аксессорам).
Но это даже близко не C#-like свойства. Смысл был бы, если бы финальный синтаксис получится бы каким-то таким
class Example {
Property<int> x = {
.get = &Example::get_x,
.set = &Example::get_y
};
};Ну, понятное дело, что ещё надо будет где-то прикопать this и всё такое и возможно для этого можно было бы немного макросов добавить.
А финальный результат из статьи - это просто очередная монструозная конструкция из макросов, такого придумали уже все подряд)
P.S.
Уже 26-й год скоро) Можно вместое enable_if использовать концепты
Правда не очень понятно зачем вам такая локализация приватных данных - с точки зерния оптимальности это не очень выгодно.
using HealthProp = Property<int, SetHealth, GetHealth>;И держали бы приватные данные прям там же в пропери. Глядишь через пару шагов ешё и в ECS какой-нибудь превратилось бы.
Ну и да, у вас же 20 стандарт включен, используйте концепты вместо enable_if
Уже лучше, но все еще храним целых три указателя. Если таких property в классе много, а самих объектов тысячи (напр. акторы в игровом движке), может быть критично по памяти.
Возьём 100 свойств на объект и 10000 акторов. Указатель на self - 8 байт + два указателя на функции-члены (16+16 = 32 байта). 40 х 100 х 10000 = 35 MiB памяти. Сомнительная экономия.
Иногда в С++ не хватает каких-то фич, которые есть в других языках. Мне, например, не хватает 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;
}
https://godbolt.org/z/e1h16r3Ye
буквально одной строкой можно доработать до 0 байт, если уже используется С++17static_assert(sizeof(Actor) == sizeof(int));
интересно, спасибо ) но мне как раз хоть какой-то размер нужен, т.к. в через рефлексию могу ссылаться на property по указателю
C#-like properties в C++ размером 1 байт