«Свойства» в классах C++

читайте также по теме: Денис Майдыковский «Свойства в C++», сообщение на форуме RSDN от _Winnie

В некоторых языках (Borland Delphi, Borland C++, MS C#, MS MC++) реализован механизм «свойств» (properties), позволяющий писать прозрачный код для контролируемого изменения и чтения полей данных. Со стороны свойство выглядит как обычное поле данных, только при присваивании ему значения (или чтении) вызывается соответсвующая функция контролируемой записи (чтения). В стандарте C++ нет свойств, но их можно реализовать самостоятельно. В статье Дениса Майдыковского «Свойства в C++» на сайте RSDN приведён пример реализации, требующей 3 дополнительных указателя на каждое «свойство». В обсуждении к статье программисты предложили вариант со всего одним лишним указателем. Моё решение также требовало по началу всего один лишний указатель на каждое «свойство», но я задумался о том, как бы вообще отказаться от лишних переменных.

101-ая попытка реализации свойств классов на стандартном языке C++.

Инкапсуляция представления объекта (его состояния, его полей данных) является ключевым моментом в ООП. Прямой неконтролируемый доступ к полям данных объектов нежелателен. Для контролированного опосредованного изменения состояния объекта применяют методы чтения/записи (get/set). С помощью этих методов реализуется безопасное изменение состояние объекта.

Реализация

При некотором размышлении над проблемой реализации свойств на ум приходят следующие факты:

Прозрачное решение

В качестве возможного решения можно описать шаблон класса, параметризованный исходным типом «свойства». Для того, чтобы экземпляр этого класса могли вызывать методы доступа, надо передать ему указатель на объект-носитель свойства, а также указатели на методы доступа. Причём все эти указатели надо передать заранее, до начала использования «свойства», то есть в конструкторе объекта-носителя. Следовательно, при таком подходе каждое новое «свойство» потребует дополнительно по 3 указателя.

Статическое программирование

Так как «свойство» описывается шаблонным классом (а шаблон можно параметризовать не только типами, но и значениями), можно передать шаблону указатели на методы доступа ещё на этапе компиляции. Главное, чтобы методы были не виртуальными, тогда их адреса будут статически известны на этапе компиляции. Но как избавиться от хранения третьего указателя — от указателя на объект-носитель «свойства»?

Немного динамики

В начале я не нашёл ничего лучшего как реализовать динамическое вычисление указателя на объект-носитель через указатель на объект самого «свойства». То есть задача свелась к определению адреса объекта через адрес его поля данных. Но компилятор готов «раскрывать» адрес поля данных объекта только при условии знания имени этого поля данных. То есть можно статически определить смещение поля данных в некотором классе, указав имя этого поля.

Но шаблон нельзя параметризовать именем поля, а передать ему статически вычисленное смещение также не удастся по простой причине: невозможно вычислить смещение ещё не описанного поля. Дело в том, что при описании поля данных мы должны конкретизировать шаблон смещением этого поля данных. Но оно ещё не описано, оно только в процессе описания. Таким образом, я пришёл к симбиозу шаблона и макроса.

Такое решение получилось небезопасным и зависящим от компилятора. В самом деле, динамическое вычисление адреса объемлющего объекта опиралось на конкретное бинарное линейное представление вложенных объектов. ');

// вычисление в классе B адреса объемлющего объекта класса А через адрес поля данных A::b класса B
B * Owner ()
{
    // берём смещение поля A::b
    B PopertyHost::*pb = & A::b;
    // базируемся относительно своего this
    return ( A* ) ( size_t(this) - *((size_t *)&pb) );

Я обратился на форум RSDN и сразу же получил ответ, который вполне удовлетворил мои требования. Трюк заключался в том, чтобы сделать «свойство» не полем данных, а методом, который возвращает объект некоторого класса, для которого перегружены необходимые операторы. При этом решается задача с определением this, так как кажый нестатический метод неявно получает this своего объекта. Предложенное решение безопасно, так как компилятор не допустит описание «свойства» за пределами класса.

// Шаблонный прототип свойства с доступом чтение/запись
template
<
    class Owner,
    class T,
    T     (Owner::*Getter) (),
    void  (Owner::*Setter) ( T )
>
struct PropertyRW_
{
            Property ( Owner & owner ): owner_ ( owner ) {}
            operator    T  ()       { return (owner_.*Getter) (); }
    T       operator    () ()       { return (owner_.*Getter) (); }
    void    operator =  ( T value ) { (owner_.*Setter) ( value ); }
    Owner & owner_;
};
 
// Макрос свойства с доступом чтение/запись
#define PropertyRW(Name, Owner, T, Setter, Getter) \
PropertyRW_<Owner, T, &Owner::Setter, &Owner::Getter> Name () \
{ \
    return PropertyRW_<Owner, T, &Owner::Setter, &Owner::Getter>(*this); \
}

Пример использования

Опишем фрагмент класса-носителя «свойства».

class XML
{
public:
//  ...
//  Методы доступа к "свойствам"
    const string & GetName ()                       { return    name_;   }
    void           SetName ( string name )          { name_   = name;    }
    const XML    * GetParent ()                     { return    parent_; }
    void           SetParent ( const XML * parent ) { parent_ = parent;  }
//  "Свойства"
                   PropertyRW ( name,   XML, const string &, GetName,   SetName );   // порождает метод name ()
                   PropertyRW ( parent, XML, const XML *,    GetParent, SetParent ); // порождает метод parent ()
private:
//  Скрытые исходные поля данных "свойств"
    string         name_;
    XML          * parent_;
};

Работа со «свойствами» будет выглядеть почти также как и с настоящими свойствами. Одно из существенных отличий заключается в том, что для вызова методов класса поля данных, которое прикрывается «свойством», необходимо явно указать дополнительные пустые скобки.

XML xml;
xml.name() = "sample";
// Преобразование к типу поля данных XML::name_ - string
cout << "XML-элемент назван " << xml.name() << endl;
// Вызов перегруженного оператора () для получения копии поля данных XML::name_
cout << "Длина имени элемента составляет " << xml.name()().length () << " символ(ов)." << endl;

Недостатки

16 февраля 2005—18 февраля 2005
Максим Проскурня
1997–2024 Axofiber, axofiber.info