Pull to refresh

Property в C++

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, а не то что мы туда пишем, что может вызвать некоторые вопросы у того, кто это впервые видит. Так же тема указателей на методы класса весьма мутная, плохо раскрытая и мало используемая. Она хорошо объяснена тут, но самое важное, что я понял, что пользоваться ими надо очень осторожно.
Получилось не так интуитивно понятно в использовании, как хотелось, но поставленная цель достигнута.

Использованные материалы:
www.rsdn.ru/article/vcpp/props.xml – основные идеи.
www.rsdn.ru/forum/cpp/854559.1.aspx – идея с правами доступа.
rsdn.ru/article/cpp/fastdelegate.xml – подробнейшее объяснение указателей на функции и методы классов в C++.

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

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

Оригинал статьи можно найти здесь. Копипаст произведён осознанно, так как тема должна быть интересна хабранаселению, а сайт, на котором статья изначально опубликована, не в состоянии выдержать даже небольшой наплыв посетителей по причине хостинга.
Tags:
Hubs:
Total votes 63: ↑50 and ↓13 +37
Views 33K
Comments Comments 87