Стратегия упаковки нескольких библиотек в C++

У меня есть класс Foo, который я не реализую напрямую, а обертываю внешние библиотеки (например, FooXternal1 или FooXternal2 ). Один из способов, который я видел для этого, - использовать директивы препроцессора как

#include "config.h"
#include "foo.h"
#ifdef _FOOXTERNAL1_WRAPPER_
//implementation of class Foo using FooXternal1
#endif

#ifdef _FOOXTERNAL2_WRAPPER_
//implementation of class Foo using FooXternal2
#endif

и config.h используется для определения этих флагов препроцессора (_FOOXTERNAL1_WRAPPER_ и _FOOEXTERNAL2_WRAPPER_). У меня сложилось впечатление, что это не одобряется сообществом программистов C++, потому что он использует директивы препроцессора, его сложно отлаживать и т. д. Кроме того, он не допускает параллельного существования обеих реализаций.

Я подумал о том, чтобы сделать Foo базовым классом и наследовать от него, чтобы обе реализации могли существовать параллельно друг с другом. Но столкнулся с двумя проблемами:

  1. Чисто виртуальные функции: cannot instatiate an object of type 'Foo', которые мне нужны во время использования.
  2. Виртуальные функции рискуют запустить объект без (надлежащей) реализации.

Я что-то пропустил? Есть ли более чистый способ сделать это?

РЕДАКТИРОВАТЬ: Подводя итог, можно сказать, что существует 3 (.5 ?!) способа сделать обертку - 2 (.5) предоставляются пакетом со льдом, а последний - Сергеем. 1- Использовать фабричные методы 2- Использовать директивы препроцессора 2.5- Используйте makefile или IDE для эффективного выполнения работы директив препроцессора 3.5- Используйте шаблоны, предложенные Сергеем

Я работаю над встроенной системой, где ресурсы ограничены, я решил использовать template<enum = default_library> со специализацией шаблона. Это легко понять для более поздних пользователей; по крайней мере я так думаю


person aiao    schedule 11.12.2012    source источник
comment
Пока вы связываете обе внешние библиотеки, предоставление виртуального класса и двух конкретных классов является отличным решением, особенно если во время выполнения вы можете определить, какую библиотеку следует использовать. Однако, если вы не связываете обе библиотеки (или если у них есть конфликты пространств имен, запрещающие это), то это не сработает. #define / #ifdef не всегда дружелюбны, но иногда это лучшее решение.   -  person mah    schedule 11.12.2012
comment
Чтобы добавить к mah, если вы используете оболочку Foo, просто объявите любые функции, которые должны быть переопределены, как чисто виртуальные. Компилятор заставит вас предоставить реализации в конкретных подклассах, как только вы попытаетесь создать экземпляры конкретных подклассов.   -  person dbv    schedule 11.12.2012
comment
именно то, что я намеревался, но тогда я не могу создать Foo, так как это абстрактный класс. Я что-то пропустил?   -  person aiao    schedule 11.12.2012
comment
Ну, похоже, вы хотите предоставить абстрактную базу с фабрикой, чтобы выбирать, какую реализацию Foo предоставлять динамически во время выполнения?   -  person Nicholas Wilson    schedule 11.12.2012
comment
@mah, нет конфликта пространств имен, я могу связать обе библиотеки. Мне нужно создать экземпляр Foo, хотя   -  person aiao    schedule 11.12.2012
comment
Вы создаете экземпляры двух подклассов Foo: FooLibA и FooLibB, верно? FooLibA и FooLibB вынуждены реализовывать любые чисто виртуальные функции, которые вы объявили в Foo.   -  person dbv    schedule 11.12.2012
comment
Нет. Foo использует либо FooXternal1, либо FooXternal2. Но на уровне абстракции я хочу использовать только Foo, независимо от базовой реализации.   -  person aiao    schedule 11.12.2012


Ответы (4)


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

class FooX1
{
public:
    void Met1()
    {
        std::cout << "X1\n";
    }
};

class FooX2
{
public:
    void Met1()
    {
        std::cout << "X2\n";
    }
};

Тогда вы можете использовать несколько вариантов.

Вариант 1. Вы можете объявить элемент шаблонного типа и обернуть все вызовы во внешнюю реализацию, даже с некоторой подготовкой перед вызовом. Не забудьте удалить impl в деструкторе ~Foo.

template<typename FooX>
class FooVariant1
{
public:
    FooVariant1()
    {
        impl=new FooX();
    }

    void Met1Wrapper()
    {
        impl->Met1();
    }
private:

    FooX *impl;
};

Применение:

FooVariant1<FooX1> bar;
bar.Met1Wrapper();

Вариант 2. Вы можете наследовать от параметра шаблона. В этом случае вы не объявляете никаких членов, а просто вызываете методы реализации по их именам.

template<typename FooX>
class FooVariant2 : public FooX
{
};

Применение:

FooVariant2<FooX1> bar;
bar.Met1();

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

person Sergey    schedule 11.12.2012
comment
Но вам все равно нужно иметь место, где вы решаете, следует ли создавать экземпляр шаблона с помощью FooX1 или FooX2 — это возвращает #ifdefs или что-то подобное. Хорошая идея использовать шаблоны, хотя - person SomeWittyUsername; 12.12.2012
comment
@Sergey Мне потребовалось некоторое время, чтобы РАЗУМИТЬ (!) что происходит, но это то, с чем я собираюсь - person aiao; 12.12.2012

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

Если вы можете решить во время компиляции, какая реализация вам нужна, у вас есть несколько вариантов:

  • Все еще используйте интерфейс. Это позволит легко перейти, если в будущем вам понадобятся обе реализации во время выполнения.

  • Используйте директивы препроцессора. Здесь нет ничего плохого, если рассматривать C++. Это чисто вопрос дизайна.

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

person SomeWittyUsername    schedule 11.12.2012

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

Должен ли тип Foo содержать какую-либо информацию, относящуюся к каждой библиотеке? Если нет, возможно, вы сможете уйти с этим:

#include "Foo.h"

#if defined _FOOXTERNAL1_WRAPPER_
    #include "Foo_impl1.cpp"
#elif defined _FOOXTERNAL2_WRAPPER_
    #include "Foo_impl2.cpp"
#else
    #error "Warn about a missing define here"
#endif

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

person Michael Kristofik    schedule 11.12.2012

Держите Foo абстрактным. Укажите фабричный метод

Foo* MakeFoo();

который выделяет новый объект типа FooImpl1 или FooImpl2 и возвращает его адрес.

Википедия о шаблоне Factory Method.

person n. 1.8e9-where's-my-share m.    schedule 11.12.2012