Ordinary Mind
21 грудня 2017 р. 16:55

Ідіома "стирання типу" на прикладі QWidget

Введіть ідіому стирання

Стаття передбачає наявність у читача базових знань C++


Відразу до діла.

Припустимо, що вам потрібно динамічно (наприклад, як мені, за натисканням кнопки) змінювати текст
кількох об'єктів, таких як:

  1. QLabel
  2. QPushButton
  3. QLineEdit

Більшість із вас знає, що ці віджети успадковують QWidget, і тому чому б не оголосити метод, який приймає вказівник на цей базовий клас, а потім уже викликати метод setText?

QWidget * base_pointer = new QLabel();
base_pointer->setText("ERROR");

Проблема в тому, що QWidget не визначений метод setText(), і тому зробити це не вдасться.

Мій знайомий порадив мені 2 варіанти вирішення цього завдання:

  • Через ідіому "стирання типу"
  • Через сигнали та слоти

Про сигнали та слоти ви вже точно знаєте, і тому я розповім про один спосіб. Коротко.

Ідіома "стирання типу" (type erasure idiom) дозволяє стерти тип будь-якого об'єкта, приховавши його за
об'єктом найбільш загального типу.

Привіт шаблони

Як це виглядає на словах:

  1. Створюємо абстрактний базовий клас А, у ньому оголошуємо віртуальну функцію SetText
  2. Наслідуємо від цього класу вже шаблонний клас, а в ньому всього лише є:

  3. поле у вигляді вказівника на тип шаблону

  4. реалізація віртуального методу SetText

  5. У класі-клієнті оголошуємо покажчик типу А

  6. Використовуємо :)

Як це виглядає насправді:

  • Абстрактний базовий клас та його спадкоємець:
namespace om_animation {
class AbstractWritableWidget {
public:
    ~AbstractWritableWidget() {}
    virtual void SetText(const QString& text) = 0;
};

template <class Widget>
class WritableWidget : public AbstractWritableWidget {
public:
    WritableWidget(Widget* writable_widget) : writable_widget_(writable_widget) {}
    ~WritableWidget() {}

    void SetText(const QString& text) override {
        writable_widget_->setText(text);
  }

private:
    Widget* writable_widget_ = nullptr;
};
}
  • Використовуємо (просто приклад):
class Client {
public:
    template <typename T>
    void Run(T * type){
        // do what you want
        writable_widget_ = new WritableWidget<T> (type);
        writable_widget_->SetText("Success");
        // do what you want
    }

private:
     AbstractWritableWidget* writable_widget = nullptr;
}
  • Реальний код:
template <typename Widget>
void om_animation::TextAnimator::RunAnimation(Widget* widget) {
  if (WritableMatcher::IsWidgetWritable(widget->metaObject()->className())) {
    if (writable_widget_) {
      delete writable_widget_;
    }
    writable_widget_ = new WritableWidget<Widget>(widget);
    timer_->start(animation_delay_);
  } else {
    throw std::logic_error(
        "incompatible type for text animation, add type to "
        "WritableMatcher class");
  }
}

У реальному коді я ще зробив клас зі статичними методами для перевірки, який клас підтримує
метод setText у бібліотеці Qt. Там просто вектор під капотом. Нічого складного.
Через метаоб'єкт отримуємо ім'я класу, порівнюємо з наявним у списку. Так – так, ні – ні.

Далі я зробив просту перевірку вказівника, і власне вже використання.
Якщо об'єкт не знайдено, клієнт отримує exception.

От і все. Я знаходжу цю ідіому дуже крутою.

Спасибі за увагу.

По статті запитували0питання

2

Вам це подобається? Поділіться в соціальних мережах!

Evgenii Legotckoi
  • 21 грудня 2017 р. 21:03

Идиома достаточно интересная, единственное, код кажется весьма громоздким.

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

В стандарте С++14 также есть возможность объявлять в качестве аргумента лямбда функции auto аргументы. Которые также шаблонизируются. И если  метод не существуют у объекта, то проект не скомпилируется.
 
Я это к чему всё. Идиома стирания типа вещь хорошая, но при наличии нескольких инструментов можно добиться хорошего баланса. Просто данная идиома из-за шаблонов весьма громоздка получается на вид.
Ordinary Mind
  • 21 грудня 2017 р. 22:25

Спасибо за отзыв. Очень приятно, что кто-то прочитал и ответил.

У меня была проблема, я хотел что бы мой код-клиент, с помощью вызова всего лишь одного метода мог изменять текстовое содержание любого виджета Qt, в котором определен метод setText. Поэтому я не мог обойтись наследованием интерфейса, под этим ведь это понимается?
class WritableWidget : public QLabel{
Q_OBJECT
public:
void setText(const QString & text) override { // something }
};
Ведь мне нужно туда передавать и QPushButton и QLineEdit и еще свои виджеты тоже, которые являются наследниками от определенных в библиотеке Qt.

Я пробовал сделать вот так:
void WritableWidget::AnimateText(QWidget * widget){
// ранее объявленный указатель как поле класса
writable_wiget_ = qobject_cast<QLabel> (widget);
}
Я не помню, но вроде все хорошо кастовалось. Но опять же, я туда передаю QLabel, но мне нужна была универсальность.

На счет 14 стандарта даже не думал . . . Пока еще с 11 не разобрался. Но спасибо за совет с лямбдами. Приму на вооружение.

Ordinary Mind
  • 21 грудня 2017 р. 22:27
writable_wiget_ = qobject_cast<QLabel*> (widget); // указатель добавил
Evgenii Legotckoi
  • 23 грудня 2017 р. 16:21

Не совсем.
Под наследованием интерфейса понимается вот что

class Interface
{
    virtual void setText(QString str) = 0;
};


class A : public QLabel, Interface
{
    virtual void setText(QString str) override
    {
        // Todo Something
    }
};

class B : public QPushButton, Interface
{
    virtual void setText(QString str) override
    {
        // Todo Something
    }
};

class C : public QWidget, Interface
{
    virtual void setText(QString str) override
    {
        // Todo Something
    }
};
То есть присутствует класс только с абстрактными методами. Все классы которые должны реализовывать данный интерфейс наследуются от данного класса, а также от тех классов, функционал которых необходим. Эти классы также можно будет использовать в рамках интерфейса приложения, как и другие виджеты.
Нужно будет только делать каст к классу интерфейса, когда понадобится и должно работать. Ну, конечно, нужно отталкиваться от архитектуры приложения. Если пихать абсолютно все виджеты, то странно получается и непонятно, откуда может прилететь ошибка в будущем.

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up