Главная
 Сайт Андрея Зайчикова
Вторник, 7 Августа 2007г. 
Карта сайта Поиск по сайту Написать письмо  
 .:Навигатор 
Новости
Библиотека
Статьи
Олимпиады
FAQ (ЧаВо)
Гостевая книга 
Ссылки
 .:Информация 


Виртуальные переменные в С++

Эта статья рассказывает, как посредством шаблонов в С++ можно создавать виртуальные переменные, то есть переменные, на самом деле возвращающие и принимающие значение при помощи предварительно заданных функций.

Прежде всего – кому и зачем нужны виртуальные переменные? А нужны они разработчикам библиотек для создания максимально красивого и простого объектного интерфейса. Концепция ООП предполагает, что сущность и описывающий ее объект неразделимы: окно, например – это просто тень соответствующего объекта, послушно повторяющая все его изменения. В этом весь смысл – создавая класс, наладить низкоуровневую реализацию, а затем полностью от нее абстрагироваться. Казалось бы, прописная истина, но на самом-то деле тот же CWnd в MFC явно организован как обыкновенный клиент GUI-сервера, хоть и говорящий с ним на строго заданную тему. Теперь посмотрим на свойства в Delphi. Две процедуры плюс языковая конструкция – и заголовок окна, цвет окна, размеры окна стали переменными объекта. Лучшего и желать нельзя. А что делать программистам на С++? То же самое. Как? Об этом и речь.

Мы воспользуемся мощнейшим из имеющихся в языке механизмов абстракции – шаблонами. Тут надо оговориться, что не все компиляторы поддерживают шаблоны в полном объеме. У пользователей GNU C++ проблем не будет (им и тестировались приведенные примеры), а вот приверженцам Visual Studio придется установить к ней "неродной" компилятор, например Intel C++.

Для начала решим простейшую задачу: создадим шаблон глобальной виртуальной переменной. Притворяться переменной нашему детищу помогут перегруженные операторы, в том числе оператор приведения типа.

template<class C, C get_value(), void set_value(C)>
struct property
{
property() {};
property(C src) { set_value(src); };
C operator= (C value)
  {
  set_value(value);
  return value;
  };
operator C() { return get_value(); };
};

А вот пример использования: пусть у нас есть функции для получения и установки года в текущей дате.

int get_year();
void set_year(int);

Создадим виртуальную переменную "номер года".

property<int, &get_year, &set_year> year;

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

А пока создадим виртуальное свойство класса. Что изменилось по сравнению с предыдущей задачей? Во-первых, функции чтения и записи теперь – члены класса. Во-вторых, надо указывать класс, с которым будет работать переменная. В-третьих, необходимо хранить указатель на "свой" объект. Итак:

template<class C, class Outer, C (Outer::*get_value)(),
         void (Outer::*set_value)(C)>
struct object_property
{
  Outer *outer;
  C operator= (C value)
  {
    (outer->*set_value)(value);
    return value;
  };
  operator C()
  {
    return (outer->*get_value)();
  };
};

Пример на этот раз возьмем более жизненный: при создании класса окна Wnd с доступом public объявлены функции get_width и set_width, оперирующие со значениeм типа int. Теперь объявим соответствующую виртуальную переменную, инстанцировав шаблон внутри описания класса:

object_property<int, Wnd, &Wnd::get_width, &Wnd::set_width> width;

Кроме того, в конструкторе класса Wnd надо будет присвоить полю width.outer значение this. Надо заметить, что заменяемость источника данных позволит использовать такую переменную как своеобразный итератор: для перехода от одного объекта к другому надо просто изменить значение поля outer.

Для работы с типами, объекты которых занимают в памяти достаточно много места, стоит описать еще два шаблона, large_property и large_object_property, для которых функция set_value принимает параметр по ссылке, чтобы объект не копировался лишний раз в стек.

Теперь перейдем к "затачиванию" шаблонов под конкретные типы и группы типов. Прежде всего –вспомним, что виртуальная переменная – это интерфейс для работы с данными, определенными вне программы. А какой тип вероятнее всего имеют такие данные? Это либо число, либо строка символов, которую при получении лучше преобразовать к соответствующей специализации (basic_string<char> или, для Unicode, basic_string<short>), либо указатель на структуру данных, либо идентификатор. В последнем случае никаких дополнений не требуется: для полноценной работы с идентификатором достаточно возможностей базового шаблона. Таким образом, остаются числа, строки и указатели.

Начнем с численных переменных. О единственной проблеме, возникающей тут, уже говорилось выше – это операторы, изменяющие значение операнда. Делать нечего – их тоже придется перегрузить. Для этого породим шаблон numeric_property. В приведенном примере перегружен только оператор "+=". Для остальных, разумеется, все аналогично.

template<class C, C get_value(), void set_value(C)>
struct numeric_property: public property<C, get_value, set_value>
{
C operator+= (C x)
  {
  C value = get_value() + x;
  set_value(value);
  return value;
  };
};

При работе с basic_string, помимо перегрузки "+=", возникает другая проблема: неявное преобразование типа не используется для операторов вызова. А значит, если вы хотите обращаться к методам своего воображаемого объекта, об этом надо позаботиться отдельно. Породим шаблон виртуальной строки, используя в качестве первого параметра тип символа строки (в примере перегружен только метод at, на практике стоит перегрузить еще по крайней мере length и empty).

template<class C, basic_string<C> get_value(),
         void set_value(basic_string<C>&)>
struct string_property: public large_property<basic_string<C>,
        get_value, set_value>
{
C at(int pos)
  {
  basic_string<C> value = get_value();
  return value.at(pos);
  };
};

Кроме того, для строк придется перегрузить операторы ввода и вывода (если в них, конечно, есть необходимость).

И, наконец, указатели. С ними тоже ничего сложного – надо просто перегрузить все операторы, связанные с разыменованием. Первый параметр шаблона – тип, указателем на который будет притворяться объект.

template<class C, C* get_value(), void set_value(C*)>
struct pointer_property: public property<C*, get_value, set_value>
  {
  C* operator->() { return get_value(); };
  C& operator*() { return *get_value(); };
  C& operator[](int i) { return get_value()[i]; };
  };

Аналогичным образом от object_property порождаются numeric_object_property и pointer_object_property, а от large_object_property – string_object_property. Вообще говоря, описанную иерархию можно сделать несколько стройнее, применив частичные специализации (подробное описание работы с ними можно найти у Страуструпа), но это не относится непосредственно к теме статьи.

Вот и все. Созданного набора шаблонов достаточно для полноценной работы с виртуальными переменными в подавляющем большинстве случаев. Надеюсь, описанная методика не будет отнесена к «искусству ради искусства»: на мой взгляд, ничто так не упрощает написание программы, как красивая иерархия классов.

Литература:
Бьерн Страуструп. Язык программирования С++ – самое полное из всех описаний С++. Там есть все, что нужно знать для прочтения этой статьи.

 
 © Андрей Зайчиков