Дизайн-шаблон для интерфейсов с поздним связыванием в C ++ 03 - PullRequest
2 голосов
/ 18 апреля 2011

В некоторых ситуациях вы хотите использовать класс в качестве реализации для интерфейса, но вы не можете изменить код или инкапсулировать этот класс (например, наследовать от него).В других ситуациях вы можете предотвратить такую ​​ситуацию.// Предположим для следующих примеров, что у нас есть следующий «запечатанный» класс, из которого мы не можем изменить код:

class my_sealed_class;
class my_lock 
{
    friend class my_sealed_class;
    my_lock() {}
    my_lock(my_lock const&) {}
};

class my_sealed_class : virtual my_lock
{
private:
    int x;

public:
    void set_x(int val) { x = val; }
    int  get_x() const  { return x; }
};

В основном, есть два вида шаблонов проектирования, которые вы можете выбрать: 1.Реализация в качестве объекта-члена интерфейса.

template<class implementation>
class my_interface0
{
private:
    implementation m_impl;

public:
    void set_x(int val) { m_impl.set_x(val); }
    int  get_x() const  { return m_impl.get_x(); }
};

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

2.) Использование шаблона CRTP без вывода из базы.

template<class derived>
class my_interface1
{
public:
    void set_x(int val) { reinterpret_cast<derived*>(this)->set_x(val); }
    int  get_x() const  { return reinterpret_cast<derived const*>(this)->get_x(); }
};

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

template<class derived> class base { void foo() { static_cast<derived*>(this)->foo(); } };
class derived : base<derived>      { void foo() {} };

К сожалению, мы не можем этого сделать.Помните, мы не можем изменить код нашего класса реализации.Давайте подумаем о том, что именно происходит с использованием my_interface1 с моей версией шаблона CRTP.В теории языка программирования C ++ 03 безопасно привести указатель производного класса к одному из его базовых классов.С другой стороны, небезопасно приводить в противоположном направлении (от базового к производному) или к совершенно другому типу.Нет гарантии, что объект памяти зарезервировал столько байтов, сколько необходимо для базовых и производных классов.Но на практике это не относится к этому примеру, потому что наш интерфейс не содержит ни одного члена.Таким образом, его размер равен 0 байтам (обратите внимание, что оператор new выделяет как минимум 1 байт, даже если класс пуст).В этом случае - практически - безопасно привести любой указатель типа к указателю my_interface1.Решающим преимуществом является то, что почти каждый компилятор будет вставлять вызовы в фактически вызванную функцию-член.

int main()
{
    // Not even worth to mention: That's safe.
    my_sealed_class msc;
    msc.set_x(1);

    // Safe, because 'my_interface0' instantiates 'my_sealed_class'.
    my_interface0<my_sealed_class> mi0;
    mi0.set_x(2);

    // Absolutely unsafe, because 'my_interface1' will access memory which wasn't allocated by it.
    my_interface1<my_sealed_class> mi1;
    mi1.set_x(3);

    // Safe, because 'my_interface1*' references an my_sealed_class object.
    my_interface1<my_sealed_class>* pmi1 = reinterpret_cast<my_interface1<my_sealed_class>*>(&msc);
    pmi1->set_x(4);

    return 0;
}

Итак, что вы считаете лучшей практикой?С наилучшими пожеланиями.

Ответы [ 2 ]

2 голосов
/ 18 мая 2011

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

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

РЕДАКТИРОВАТЬ: Чтобы ответить на последний вопрос исходного вопроса: никогда не будет лучшей практикой использовать вариант 2 над вариантом 1. Если есть проблемы с производительностью с вариантом 1, вероятно, есть более эффективные способы решения. их, чем неопределенное поведение.

0 голосов
/ 20 апреля 2011

Поэтому, если я понимаю, вы не можете использовать наследование (производное от my_sealed_class), потому что его методы не объявлены виртуальными, и вы не можете его изменить. Вы пробовали частное наследство?

В этом случае я бы выбрал композицию option (1), потому что она проще и проще для чтения, а компиляторы ее довольно хорошо оптимизируют. Я также думаю, что это то, что делает STL.

(2) только выглядит * на 1007 * быстрее, эти повторные интерпретации будет не так просто оптимизировать, а код немного сложно прочитать.

...