C++. Убираем приватные поля из описания класса или немного дурачества

в 15:30, , рубрики: c++, firewall, pimpl, КодоБред, ненормальное программирование, Программирование
C++. Убираем приватные поля из описания класса или немного дурачества - 1

Всем привет! Решил на выходных продолжить писать свой домашний проект и наступила пора реализовать платформозависимый код. Самым простым вариантом было бы описать классы в *.h файле, а в зависимости от платформы, закрытые поля засунуть под #define. При этом, саму реализацию по конкретным платформам разнести по *.cpp файлам и включать их в компиляцию в зависимости от текущей платформы. Но... мне не нравится как выглядит описание класса с #define, поэтому я решил убрать препроцессор и оставить в описании класса только интерфейс. И да, я не пользовался абстрактными классами и pimpl, всё еще хуже)

Дисклеймер

Основной язык автора С.

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

Продолжим. Какие варианты здесь приходят сразу на ум. У меня два:

  1. Использовать абстрактный класс с виртуальными методами, затем наследовать его и реализовать эти методы. Добавить статичный метод для создания конкретного экземпляра.

  2. Воспользоваться техникой pimpl (Pointer to Implementation), когда в исходном классе только указатель на другой класс/структуру, который(ая) реализует требуемый функционал или содержит поля с данными.

Первый вариант мне не нравится поскольку на ровном месте создается ненужная дополнительная нагрузка (тот самый overhead) в виде таблицы виртуальный методов. По сути это указатели на функции. Возможно при некой оптимизации какой-нибудь компилятор додумается оптимизировать вызовы, возможно.

Второй вариант тоже не нравится, ибо для доступа к полям скрытого класса нужно каждый раз считать еще одно смещение. К тому же здесь есть дополнительная нагрузка в виде вызова new/malloc для скрытого класса/структуры.

Как бы было проще если можно было бы так
// A.h

class A
{
private:
  static const size_t _sizeofdata;
  unsigned char _data[_sizeofdata];
};

// A.cpp

struct LinuxSpecifiecData
{
  int a, b, c;
};

const size_t A::_sizeofdata = sizeof(LinuxSpecifiecData);

Но ясен пень так нельзя

И тут я подумал, а можно ли реализовать класс, который будет без этих дополнительных нагрузок и при этом не надо будет в описании класса декларировать закрытые поля. Тут я задумался как бы я сделал это на чистом С и у меня возникла идея. Экстремально простая. Естественно, не без минусов, но об этом позже.

Необходимо при вызове new для класса выделять память для другого (скрытого) класса, ну или структуры. К примеру имеется у нас описание класса со следующими методами (файл Render.h):

class Render
{
public:
  void clear();
  void present();
  void clipRegion(int x, int y, int width, int height);
  void drawLine(int x1, int y1, int x2, int y2, unsigned int color);
  void drawRectangle(
    int x, int y, int width, int height,
    unsigned int cornerRadius,
    unsigned int solidColor, unsigned int borderColor);
  void drawPicture(int x, int y, int width, int height, int pictureIndex);
  void drawText(
    int x, int y, int width, int height,
    int fontIndex,
    const char *text, unsigned int textLength,
    unsigned int foregroundColor, unsigned int backgroundColor);
};

У каждой платформы своя реализация и свои закрытые поля у этого класса, но объявлять мы их здесь не будем. Лучше объявим их в новой структуре в исходном файле, к примеру LinuxRender.cpp:

...
struct LinuxRenderData
{
  int a, b, c;
};
...

Теперь переопределим для класса Render операторы new и delete.

// Render.h
class Render
{
public:
...
  void *operator new(std::size_t size);
  void operator delete(void *ptr);
...
};

// LinuxRender.cpp
...
void *Render::operator new(std::size_t size) {
  return malloc(sizeof(LinuxRenderData));
}

void Render::operator delete(void *ptr) {
  free(ptr);
}
...

Теперь у нас при создании экземпляра объекта выделяется память для скрытой структуры. И мы можем использовать данные этой структуры в методах класса Render. К примеру добавим метод initialize.

// Render.h
class Render
{
public:
...
  int initialize();
...
};

// LinuxRender.cpp
...
int Render::initialize() {
  LinuxRenderData *pdata = (LinuxRenderData *)this;
  pdata->a = pdata->b = pdata->c = 0;
  return 0;
}
...

Вот собственно и вся идея. Но, воскликнет читатель, что будет если класс Render будет создан не через new, а, к примеру, в стеке или глобально. И тут начинаются минусы. Тут я скажу что всё сломается) Ибо sizeof(Render) будет 1, поскольку полей класс Render не имеет. Лепим костыли. Поэтому надо запретить создавать класс не через new. Для этого делаем конструкторы закрытыми. А также создадим статичный метод create для создания экземпляров класса.

// Render.h
class Render
{
public:
  static Render *create();
...
private:
  Render();
  Render(const Render &);
...
};

// LinuxRender.cpp
...
Render *Render::create() {
  return new Render();
}
...

Здесь можно добавить ещё один костыль в виде статичного метода sizeOf, ибо sizeof не будет выдавать правильное значение.

// Render.h
class Render
{
public:
  static size_t sizeOf();
...
};

// LinuxRender.cpp
...
size_t Render::sizeOf() {
  return sizeof(LinuxRenderData);
}
...

Итоги

Я, конечно, получил что хотел. Но здесь есть минусы:

  1. Автоматически вычисленные фишки компилятора, такие как sizeof или еще какие-либо операции над типом работать не будут. И тут надо четко понимать что вы делаете, ибо можно выстрелить себе во все места на ровном месте.

  2. Необходимость переопределять для каждого класса оператор new

  3. Нельзя выделять память под экземпляр объекта без new (по умолчанию, стек, глобальная память работать не будут)

  4. Необходимость создания метода для каждого подобного класса, который будет создавать экземпляр этого класса

  5. Еще не очень удобно в каждом методе класса приводить тип к скрытому классу/структуре (LinuxRenderData *pdata = (LinuxRenderData *)this)

Поэтому не рекомендую пользоваться никому. Рассматривайте статью как запоздалый пятничный юмор)

Автор:
Fen1xL

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js