Принудительно переопределить все виртуальные функции из родительского класса из дочернего класса

Мы оборачиваем объект, реализующий абстрактный класс IFunctionality, в класс, который мы пишем, который также реализует IFunctionality.

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

Наша обертка выглядит так:

class WrapperFunctionality : public IFunctionality
{
public:
    WrapperFunctionality(IFunctionality& pOriginal)
        : m_pOriginal(pOriginal)
    {
    }

    // We override all virtual functions and forward them to the original.
    void doX() override { m_pOriginal->doX(); }
    void doY() override { m_pOriginal->doY(); }
    // ...

    // Well, except a few functions that we specialize.
    // That's why we need the wrapper.
    void doSomethingSpecial() override { ... }
};

На данный момент все в порядке. Но код может сломаться в будущем.

Проблема 1: сторонний код может добавлять новые нечистые виртуальные функции в IFunctionality. Это останется незамеченным, и мы не сможем вызвать соответствующую функцию в исходном объекте.

Проблема 2: сторонний код может добавлять общедоступные члены в IFunctionality и иметь прямой доступ к этим членам. Мы также не смогли бы передать эти модификации исходным объектам.

Интересно, сможем ли мы обнаружить эти проблемы во время компиляции с помощью static_assert или какой-нибудь магии шаблонов.

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

Но проблема 1, скорее всего, произойдет. Класс интерфейса уже имеет нечистые виртуальные функции и, вероятно, получит новые. Есть ли способ обеспечить, чтобы мой класс реализовывал все виртуальные функции (чистые или нет) из этого класса?


Чтобы прояснить ситуацию, позвольте мне дать более конкретное описание ситуации.

Интерфейс представляет собой поверхность, на которой можно рисовать геометрию с заданными атрибутами:

class ISurface
{
public:
    virtual void setColor(const RGB& color) = 0;
    virtual void setOpacity(float alpha) = 0;
    virtual void drawLine(...) = 0;
    virtual void drawCircle(...) = 0;
    // etc
};

Конкретная реализация ISurface создается сторонней структурой, и существует несколько ее реализаций, основанных на серверной части.

У нас есть много объектов в нашей системе, которые умеют рисовать себя на ISurface. Мы не контролируем их draw функцию. Объекты могут быть из сторонних плагинов:

class IDrawableObject
{
public:
    virtual void draw(ISurface* pSurface) const = 0;
};

Затем возможная реализация IDrawableObject, возможно, из стороннего кода:

class SomeObject : public IDrawableObject
{
public:
    virtual void draw(ISurface* pSurface) const override
    {
        pSurface->setColor(RGB(255, 0, 0));
        pSurface->drawLine(...);
        pSurface->setOpacity(0.7f);
        pSurface->fillCircle(...);
    }
};

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

// That function will be called by the framework
virtual void drawObjectToSurface(const IDrawableObject* pObject, ISurface* pSurface) override
{
    pSurface->setColor(m_overrideColor);
    pSurface->setOpacity(m_overrideAlpha);

    // Next line does not work as expected, because the object
    // overwrites our color and alpha before drawing itself
    //pObject->draw(pSurface); // does not work

    // Instead, we use a wrapper that blocks color and opacity writes
    BlockingSurfaceWrapper wrapperSurface(pSurface);
    pObject->draw(&wrapperSurface);
}

person myavuzselim    schedule 28.01.2021    source источник
comment
Именно поэтому ортогональные концепции не должны реализовываться виртуально; это делает расширение очень трудным. Жаль, что сторонние разработчики так и сделали.   -  person Alexander Guyer    schedule 28.01.2021
comment
Не могли бы вы уточнить? Какие здесь ортогональные понятия?   -  person myavuzselim    schedule 28.01.2021
comment
Опечатка: я сказал практически, но я имел в виду вертикально. Если есть какая-либо законная причина изменить реализацию ISurface при сохранении его интерфейса, то для начала следует разделить интерфейс и реализацию. т. е. ISurface должен только иметь интерфейс (чистые виртуальные функции), а какой-то другой класс, скажем, SimpleSurface, может предоставить некоторые базовые реализации, которые при желании можно использовать/наследовать. Таким образом, ваш BlockingSurfaceWrapper может просто унаследовать ISurface при обертывании SimpleSurface, и проблема будет решена тривиально.   -  person Alexander Guyer    schedule 28.01.2021


Ответы (2)


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

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

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

В обзорах учтены все ваши опасения, если что-то непонятно:

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

  2. сторонний код может добавлять общедоступные члены в IFunctionality и иметь прямой доступ к этим членам — то же самое, проверка кода это выявит.

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

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

Это либо расширение обзоров кода, либо реализация статического анализа. Нет пути вокруг этого.

person Kuba hasn't forgotten Monica    schedule 28.01.2021

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

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

Но я не понимаю, как переопределение некоторых виртуальных методов требует наличия оболочки. Вы можете пометить их как final и предоставить аналогичный интерфейс с другими чисто виртуальными методами. В конце концов, что произойдет, если pOriginal сам их переопределит? Вы игнорируете их? Я бы сказал, что с этой конструкцией что-то не так.

person Quimby    schedule 28.01.2021
comment
Спасибо за ответ. Действительно, доступ к vtables не будет переносимым и может быть громоздким. - person myavuzselim; 28.01.2021
comment
Но я не понимаю, как переопределение некоторых виртуальных методов требует наличия оболочки. Я обновил вопрос, предоставив больше информации о конкретной ситуации. - person myavuzselim; 28.01.2021