суббота, 25 июня 2011 г.

Property C++

Наверное, все любители языка C++, которые использовали другие языки, такие как C#, удивляются: почему же в плюсах нет property. Ведь это действительно удобное средство, позволяющее полностью контролировать доступ к членам класса. В общем и я недавно заинтересовался данным вопросом. Подумав, полистав Страуструпа и наконец, погуглив, я пришёл к выводу, что property можно реализовать средствами языка. Думаю многие уже видели разнообразные реализации, например, от microsoft, но для кого-то, надеюсь, это будет интересным открытием.

Постановка задачи

Для начала определимся, что мы хотим. А хотим мы, чтобы к свойству можно было обращаться как к обычному полю объекта через точку (ну или ->) как на запись, так и на чтение и без всяких скобочек. То есть вот так:

a.property = value;
MyClass var = a.property;

При этом должны вызываться определённые нами функции сеттер и геттер. Ещё хотелось бы, чтобы в классе свойство определялось максимально просто и понятно.


Реализация

Мы видим, что если a.property будет экземпляром определённого нами класса Property, то можно перегрузить оператор =, получив сеттер, а так же оператор приведения типов, получив геттер. Сам этот класс должен быть шаблонным, чтобы позволить использовать свойства любого типа.
Для того чтобы не реализовывать для каждого property свой класс, перегруженные операторы должны производить вызов функции по указателю, переданному свойству в конструкторе.
Для использования мы объявляем переменную нашего типа Property как public поле в классе и передаём тип, который нам нужен, в качестве первого параметра шаблона, а указатели на функции в параметрах конструктора.
Когда это всё было реализовано, то вспомнилась ещё одна замечательная особенность property: оно может быть Read Only. До этого рассматривалось только полноценное property с сеттером и геттером. И важно, чтобы права на чтение и запись проверялись на этапе компиляции. Здесь к нам на помощь приходят специализации шаблонов. Можно реализовать три различных класса (чтение и запись, только чтение, только запись) Property с одинаковыми именами, отличающиеся параметром шаблона, так называемой специализацией. Итоговая реализация выгладит вот так:
#pragma once
 
#define NULL 0
 
class ReadOnly;
class WriteOnly;
class ReadWrite;
 
 
template <typename Type, typename Owner,typename Access>
class Property
{
 
};
 
template<typename Type, typename Owner> 
class Property<typename Type, typename Owner, ReadWrite>
{
protected:
    typedef Type (Owner::*getter)();
    typedef void (Owner::*setter)(Type);
    Owner * m_owner;
    getter m_getter;
    setter m_setter;
public:
    // Оператор приведения типа. Реализует геттер.
    operator Type()
    {
        return (m_owner->*m_getter)();
    }
    // Оператор присваивания. Реализует сеттер.
    void operator =(Type data)
    {
        (m_owner->*m_setter)(data);
    }
 
    Property() :
  m_owner(NULL),
        m_getter(NULL),
        m_setter(NULL)
    {
    }
 
    Property(Owner * const owner, getter getmethod, setter setmethod) :
        m_owner(owner),
        m_getter(getmethod),
        m_setter(setmethod)
    {
    }
 
    void init(Owner * const owner, getter getmethod, setter setmethod)
    {
        m_owner = owner;
        m_getter = getmethod;
        m_setter = setmethod;
    }
};
 
 
template<typename Type, typename Owner> 
class Property<typename Type, typename Owner, ReadOnly>
{
protected:
    typedef Type (Owner::*getter)();
    Owner * m_owner;
    getter m_getter;
public:
    // Оператор приведения типа. Реализует геттер.
    operator Type()
    {
        return (m_owner->*m_getter)();
    }
 
 
    Property() :
        m_owner(NULL),
        m_getter(NULL)
    {
    }
 
    Property(Owner * const owner, getter getmethod) :
        m_owner(owner),
        m_getter(getmethod)        
    {
    }
 
    void init(Owner * const owner, getter getmethod)
    {
        m_owner = owner;
        m_getter = getmethod;
    }
};
 
 
 
template<typename Type, typename Owner> 
class Property<typename Type, typename Owner, WriteOnly>
{
protected:
    typedef void (Owner::*setter)(Type);
    Owner * m_owner;
    setter m_setter;
public:
    // Оператор присваивания. Реализует сеттер.
    void operator =(Type data)
    {
        (m_owner->*m_setter)(data);
    }
 
    Property() :
        m_owner(NULL),
        m_setter(NULL)
    {
    }
 
    Property(Owner * const owner, setter setmethod) :
        m_owner(owner),
        m_setter(setmethod)
    {
    }
 
    void init(Owner * const owner, setter setmethod)
    {
        m_owner = owner;
        m_setter = setmethod;
    }
};

Использование



Использовать можно вот так:

Файл TestClass.h

#pragma once
#include "Property.h"
 
class TestClass
{
public:
 TestClass(void);
 ~TestClass(void);
 
 void _setterRW(int a);
 int _getterRW();
 Property<int, TestClass, ReadWrite> testRW;
 
 int _getterRO();
 Property<int, TestClass, ReadOnly> testRO;
 
 void _setterWO(int a);
 Property<int, TestClass, WriteOnly> testWO;
 
private:
 int propRW;
 int propRO;
 int propWO;
};

Файл TestClass.cpp

#include "TestClass.h"
 
TestClass::TestClass(void)
{
 testRW.init(this, &TestClass::_getterRW, &TestClass::_setterRW);
 
 testRO.init(this, &TestClass::_getterRO);
 propRO = 123;
 
 testWO.init(this, &TestClass::_setterWO);
}
 
int TestClass::_getterRW()
{
 return propRW;
}
 
void TestClass::_setterRW(int a)
{
 propRW = a; 
}
 
int TestClass::_getterRO()
{
 return propRO;
}
 
void TestClass::_setterWO(int a)
{
 propWO = a;
}
 
TestClass::~TestClass(void)
{
}

Файл main.cpp
#include "TestClass.h"
#include "stdio.h"
 
int main()
{
 TestClass t;
 t.testRW = 15;
 int a = t.testRW;
 t.testWO = 34;
 //a = t.testWO; //ошибка: чтение WriteOnly property
 //t.testRO = 45; //ошибка: запись в ReadOnly property
 printf("RW = %d\n", int(t.testRW));
 printf("RO = %d\n", int(t.testRO));
 scanf("%d", a);
 return 0;
}


Выводы

Эта реализация меня устраивает, но не нравится по нескольким причинам. Первая – необходимость указывать класс, к которому относится property. Вторая – необходимость инициализации каждого свойства в конструкторе, а то есть на этапе выполнения. Третья – невозможность создания статических property. Ну и четвёртая, менее существенная: подсказчик показывает тип Property, а не то что мы туда пишем, что может вызвать некоторые вопросы у того, кто это впервые видит. Так же тема указателей на методы класса весьма мутная, плохо раскрытая и мало используемая. Она хорошо объяснена тут(ссылка), но самое важное, что я понял, что пользоваться ими надо очень осторожно.
Получилось не так интуитивно понятно в использовании, как хотелось, но поставленная цель достигнута.
Использованные материалы:
http://www.rsdn.ru/article/vcpp/props.xml – основные идеи.
http://www.rsdn.ru/forum/cpp/854559.1.aspx – идея с правами доступа.
http://rsdn.ru/article/cpp/fastdelegate.xml – подробнейшее объяснение указателей на функции и методы в C++.

Исходники с примерами тут.
Проект Visual Studio 2008 тут .

Данный текст опубликован под лицензией CC-BY.

Комментариев нет:

Отправить комментарий