2010-07-05 4 views
0

Я строю иерархию объектов, которые обертывают примитивные типы, например целые числа, булевы, поплавки и т. Д., А также типы контейнеров, такие как векторы, карты и наборы. Я пытаюсь (иметь возможность) создавать произвольную иерархию объектов и иметь возможность легко устанавливать/получать свои значения. Эта иерархия будет передана другому классу (не упомянутому здесь), и из этого представления будет создан интерфейс. Это цель этой иерархии, чтобы иметь возможность создать представление GUI из этих objects.To быть более точным, у меня есть что-то вроде этого:Как создать правильную иерархию объектов в C++

class ValObject 
{ 
    public: 
     virtual ~ValObject() {} 
}; 

class Int : public ValObject 
{ 
    public: 
     Int(int v) : val(v) {} 
     void set_int(int v) { val = v); 
     int get_int() const { return val; } 
    private: 
     int val; 
}; 

// other classes for floats, booleans, strings, etc 
// ... 

class Map : public ValObject {} 
{ 
    public: 
     void set_val_for_key(const string& key, ValObject* val); 
     ValObject* val_for_key(const string& key); 
    private: 
     map<string, ValObject*> keyvals; 
}; 

// classes for other containers (vector and set) ... 

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

Основная проблема, с которой я столкнулся, заключается в том, как установить/получить значения через указатель на базовый класс ValObject. Сначала я подумал, что могу просто создать множество функций в базовом классе, например set_int, get_int, set_string, get_string, set_value_for_key, get_value_for_key и т. Д. И заставить их работать только для правильных типов. Но тогда у меня было бы много случаев, когда функции ничего не делали и просто загрязняли мой интерфейс. Моя вторая мысль была создавать различные прокси-объекты для установки и получения различных значений, например,

class ValObject 
{ 
    public: 
     virtual ~ValObject() {}  
     virtual IntProxy* create_int_proxy(); // <-- my proxy 
}; 

class Int : public ValObject 
{ 
    public: 
     Int (int v) : val(v) {} 
     IntProxy* create_int_proxy() { return new IntProxy(&val); } 
    private: 
     int val; 
}; 

class String : public ValObject 
{ 
    public: 
     String(const string& s) : val(s) {} 
     IntProxy* create_int_proxy() { return 0; } 
    private: 
     string val; 
}; 

Клиент может затем использовать этот прокси-сервер для установки и получения значений в Int через ValObject:

ValObject *val = ... // some object 
IntProxy *ipr = val->create_int_proxy(); 
assert(ipr); // we know that val is an Int (somehow) 
ipr->set_val(17); 

Но с этим дизайном у меня все еще слишком много классов для объявления и реализации в различных подклассах. Правильно ли это? Есть ли альтернативы?

спасибо.

+3

Я думаю, что это определенно неправильный дизайн и слишком сложный. Чего вы пытаетесь достичь? – Philipp

+0

Я пытаюсь (иметь возможность) создавать произвольную иерархию объектов и иметь возможность легко устанавливать/получать их значения. Эта иерархия будет передана другому классу (не упомянутому здесь), и из этого представления будет создан интерфейс. Это цель этой иерархии, чтобы иметь возможность создавать представление GUI из этих объектов. – GavinTael

+1

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

ответ

3

Посмотрите на boost::any и boost::variant на существующие решения. Ближайшим к тому, что вы предлагаете, является boost::any, и код достаточно прост, чтобы читать и понимать, даже если вы хотите создать собственное решение для учебных целей. Если вам нужен код, не изобретайте велосипед, используйте boost::any.

+0

Boost :: любое не то же самое решение. Boost :: any - это решение типа ценности - он полиморфен. Эти два совершенно разные. – Puppy

+0

@DeadMG: Взгляните на реализацию boost :: any. На самом деле это очень похоже на то, что он предлагает. (Ручная разметка и игнорирование оптимизации и деталей) Объект 'any' сохраняет указатель на базовый класс, который предлагает только виртуальный деструктор и метод для идентификации типа. «Any_cast» проверяет правильность типа и выполняет «dynamic_cast» внутреннего указателя на производный тип (фактические производные типы - это экземпляры шаблона) –

+0

@David: Это не поможет ему, когда он захочет сделать new Int и передать этот указатель. Потому что boost :: any - это значение, и ему нужна ссылка. – Puppy

0

Используйте dynamic_cast для запуска иерархии. Вам не нужно предоставлять явный интерфейс для этого - любой разумный программист на C++ может это сделать. Если они не могут этого сделать, вы можете попробовать перечислить разные типы и создать для них интегральную константу, которую вы можете затем предоставить виртуальной функции для возврата, а затем static_cast.

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

struct functor { 
    void operator()(Int& integral) { 
     ... 
    } 
    void operator()(Bool& boo) { 
     ... 
    } 
}; 
template<typename Functor> void PerformOperationByFunctor(Functor func) { 
    if (Int* ptr = dynamic_cast<Int*>(this)) { 
     func(*ptr); 
    } 
    // Repeat 
} 

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

+1

Это не очень масштабируется с * должен иметь возможность создавать и произвольную иерархию объектов *.Вы вынуждаете клиентов переписывать «PerformOperationByFunctor», чтобы добавить каждый тип иерархии, а также подвержен ошибкам в том, что если есть объекты на разных уровнях иерархии (car, ford, ford mondeo), пользователь должен быть осторожен предоставить блоки if if else, упорядоченные от наиболее подробных до менее подробных типов в иерархии. –

+0

Я знаком с этим шаблоном, но я не знаю, сколько бремени будет сравниваться с моей реализацией. Что предпочитает клиент? – GavinTael

+0

Клиент пишет свои внутренние функции сейчас? PerformOperationByFunctor существует в ValObject *. Кроме того, нет необходимости в том, чтобы он существовал в первую очередь. Dynamic_Cast отлично справляется с этим. – Puppy

1

Одна из красавиц C++ заключается в том, что такие навязчивые решения часто не нужны, но, к сожалению, мы по-прежнему видим подобные, которые реализуются сегодня. Вероятно, это связано с преобладанием Java, .NET и QT, который следует за такими типами моделей, где у нас есть общий класс объектов, который унаследован почти всем.

Навязчивым является то, что используемые типы должны быть изменены для работы с совокупной системой (наследуя от базового объекта в этом случае). Одной из проблем с навязчивыми решениями (хотя иногда и целесообразно) является то, что они требуют связывания этих типов с системой, используемой для их агрегирования: типы становятся зависимыми от системы. Для POD невозможно напрямую использовать интрузивные решения, так как мы не можем изменить интерфейс int, например: требуется обертка. Это также относится к типам вне вашего контроля, таким как стандартная библиотека C++ или boost. В результате вы тратите много времени и усилий на создание оболочек для всех видов вещей, когда такие оболочки могут быть легко сгенерированы на C++. Это может также быть очень пессимистичным для вашего кода, если навязчивое решение будет равномерно применяться даже в тех случаях, когда это излишне и наносит ущерб служебным или временным ресурсам.

С C++ для вас доступно множество неинтрузивных решений, но это особенно актуально, когда мы знаем, что мы можем комбинировать статический полиморфизм с использованием шаблонов с динамическим полиморфизмом с использованием виртуальных функций. В принципе мы можем генерировать эти обертки, основанные на базовом объекте, с виртуальными функциями «на лету» только для случаев, когда это решение необходимо без пессимизации случаев, когда это необязательно.

Как уже было сказано, boost :: any - отличная модель того, чего вы хотите достичь. Если вы можете использовать его напрямую, вы должны его использовать. Если вы не можете (например: если вы предоставляете SDK и не можете зависеть от третьих сторон, чтобы иметь соответствующие версии boost), посмотрите на это решение в качестве рабочего примера.

Основная идея boost :: any заключается в том, чтобы сделать что-то похожее на то, что вы делаете, только эти обертки сгенерировали во время компиляции. Если вы хотите сохранить int в boost :: any, класс будет генерировать класс оболочки int, который наследует от базового объекта, который предоставляет виртуальный интерфейс, необходимый для создания , любой работает во время выполнения.

Основной проблемой я столкнулся в том, как набор/получить значения через указатель на базовый класс ValObject. Во-первых, я думал, что я мог бы просто создать много функций в базовом классе, как set_int, get_int, set_string, get_string, set_value_for_key, get_value_for_key, и т.д., и сделать их работу только для правильных типов. Но then, у меня было бы много случаев, когда ничего не делают и только загрязняют мой интерфейс.

Как вы уже правильно вывели, это, как правило, было бы уступает дизайну. Один контрольный признак наследования используется ненадлежащим образом - это когда у вас много базовых функций, которые не применимы к вашим подклассам.

Рассмотрите дизайн потоков ввода-вывода. У нас нет потоков с такими функциями, как output_int, output_float, output_foo и т. Д., Как непосредственные методы в ostream. Вместо этого мы можем перегрузить оператора < < для вывода любого типа данных, который мы хотим, без вмешательства. Аналогичное решение может быть достигнуто для вашего базового типа. Вы хотите связать виджеты с пользовательскими типами (например: пользовательский редактор свойств)? Мы можем это позволить:

shared_ptr<Widget> create_widget(const shared_ptr<int>& val); 
shared_ptr<Widget> create_widget(const shared_ptr<float>& val); 
shared_ptr<Widget> create_widget(const shared_ptr<Foo>& val); 
// etc. 

Вы хотите сериализовать эти объекты? Мы можем использовать такое решение, как потоки ввода-вывода.Если вы адаптируете свое собственное решение, например boost :: any, можно ожидать, что такие вспомогательные функции уже существуют с сохраненным типом (виртуальные функции в сгенерированном классе-оболочке могут вызвать create_widget (T), например

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

Во всяком случае, это до вас, но считает ненавязчивый подход к этому, как это обычно будет менее проблематичным и в целом гораздо более гибкими.

Смежные вопросы