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


  1. OldFisher
    14.10.2025 17:43

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


  1. Myz17
    14.10.2025 17:43

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


    1. 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 };


    1. Einherjar
      14.10.2025 17:43

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


  1. tenzink
    14.10.2025 17:43

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

    В статье сеттер вызывает функцию UpdateVisual, которая скорее всего заметно тяжелее присваивания матриц. И вот тут начинаются проблемы. Программисты на C++ от конструкции `a.transform = some_matrix` обычно не ждут подвоха в отличии от a.SetTransform(some_matrix). Совершенно нормально написать такой код, возможно даже шаблонный

    for(const auto& tr : transforms) {
      a.transform *= tr;
    }

    И получить premature pessimization на ровном месте


    1. uvic
      14.10.2025 17:43

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

      Я искал причину тормозов две недели. Только по ассемблерному листингу понял, что простое обращение к члену класса вызывает что-то подозрительное ;))

      Так я познакомился с property в Дельфи ;)))


    1. 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 костей, как-то надо посчтать все анимации, кинуть предпосчитанные ключи матриц(бейкинг всех анимаций модельки), чтобы счет был только в ГПУ на весах по фрейму, чтобы можно было тоже как флай обджекты рендерить если есть повторные модельки


  1. segment
    14.10.2025 17:43

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


  1. 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; }


  1. 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);
    


  1. 73xc
    14.10.2025 17:43

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


  1. 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 использовать концепты


  1. domix32
    14.10.2025 17:43

    Правда не очень понятно зачем вам такая локализация приватных данных - с точки зерния оптимальности это не очень выгодно.

    using HealthProp = Property<int, SetHealth, GetHealth>;

    И держали бы приватные данные прям там же в пропери. Глядишь через пару шагов ешё и в ECS какой-нибудь превратилось бы.

    Ну и да, у вас же 20 стандарт включен, используйте концепты вместо enable_if


  1. ReadOnlySadUser
    14.10.2025 17:43

    Уже лучше, но все еще храним целых три указателя. Если таких property в классе много, а самих объектов тысячи (напр. акторы в игровом движке), может быть критично по памяти.

    Возьём 100 свойств на объект и 10000 акторов. Указатель на self - 8 байт + два указателя на функции-члены (16+16 = 32 байта). 40 х 100 х 10000 = 35 MiB памяти. Сомнительная экономия.


    1. anz Автор
      14.10.2025 17:43

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


      1. ReadOnlySadUser
        14.10.2025 17:43

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


        1. anz Автор
          14.10.2025 17:43

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


  1. 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;
    }
    


    1. anz Автор
      14.10.2025 17:43

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

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

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


  1. UdUser
    14.10.2025 17:43

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


    1. anz Автор
      14.10.2025 17:43

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


      1. UdUser
        14.10.2025 17:43

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