Pull to refresh

Comments 23

Со временем я пришёл к убеждению, что когда кажется, что нужны property, надо первым делом проверить, нет ли грязи в архитектуре. Просто потому что они тянут "кишочки" (т.е. детали реализации) наружу, в интерфейс. Хотя формально это оборачивается в "защитные" методы-прокладки, семантически происходит как раз это. А значит, нарушаются уровни абстракции и надо посмотреть, что перекособочено и поправить, пока не стало поздно и дорого.

Вообще-то, идея, насколько помню, была от 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 памяти. Сомнительная экономия.

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

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

Очень даже тянут )

Иногда в С++ не хватает каких-то фич, которые есть в других языках. Мне, например, не хватает 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;
}

А нехватает для чего?

Так вот в начае же как раз, чтобы иметь нормальный синтаксис математических операций

actor.transform = left.transform righ.transform up.transform;

https://godbolt.org/z/e1h16r3Ye
буквально одной строкой можно доработать до 0 байт, если уже используется С++17
static_assert(sizeof(Actor) == sizeof(int));

интересно, спасибо ) но мне как раз хоть какой-то размер нужен, т.к. в через рефлексию могу ссылаться на property по указателю

Да должно работать и с рефлексией, уникальный адрес нужен только, если у вас экзотическая реализация с проверкой полей на уникальность по их адресу. Но я ошибся, нужен всё-таки C++20

Sign up to leave a comment.

Articles