Как стать автором
Обновить

Комментарии 43

Почему бы cl##name ob##name тоже не сделать static?
Тогда populate_statdata и VcfInitializer будут не нужны
Одна голова хорошо, а две — лучше! Надо будет попробовать. В самом деле, можно избавиться от нагрузки на создание структур во время выполнения программы.
Приятная статья, спасибо!
Еще можно добавить макрос DECLARE_MEMBER_PARAMSTRUCT_PTR для глубокого сравнения указателей

bool comp##name(const ThisParamFieldClass& a) const \
{ \
return (*name) == (*a.name); \
}
Такие структуры не предназначены для хранения указателей, потому что тогда они перестают быть чисто структурами данных. Для них также теряет смысл копирование операторами по умолчанию.
Генерация кода C++. Описание структуры на C++ и оператора ее сравнения не пишется программистом вручную, а генерируется скриптом на основе описания структуры на каком-то другом входном языке. Данный подход, с моей точки зрения, является идеальным...

С моей точки зрения тоже. Почему вы отказались от такого решения?
Затрудняюсь ответить. Так сложилось исторически. Ну а сейчас оно работает, поэтому пока нет стимула реализовывать другие решения!
Меня вот искренне удивляют такие статьи. Вопрос к автору: вы действительно готовы обьявлять параметры как
DECLARE_MEMBER_PARAMSTRUCT(double,                        karma);

вместо
double karma;

?
Просто, я бы не согласился с тем, что мешанина макросов в обьявлении структур данных лучше, чем написать руками оператор сравнения.
Это до тех пор пока вы не потратите день пытаясь понять почему все перестало работать, хотя вы только добавили новую переменную в класс, но забыли внести ее в оператор сравнения. Разумеется такие баги редко выстреливают сразу же, поэтому обнаружится это через неделю, когда вы уже давно забудете о той переменной и честно будете искать ошибки в реализации двоичного поиска, или в других местах.
Понимаю. Просто, дело в том, что если класс уже отлажен, то в него нельзя добавлять новые поля. Иначе, он автоматически становится неотлаженным. А для неотлаженных классов существует процесс тестирования. Если меняешь оттестированный код — протестируй его заново, я следую этому принципу, и у меня не может возникнуть описанной вами ситуации, отсюда мое недоумение.
Нет, ну бывают еще баги в тестах, но по этой дорожке можно далеко от темы уйти.
скажем так, широкоизвестный макрос
#define GETSET(type, name, name2) \
private: type name; \
public: TypeTraits<type>::ParameterType get##name2() const { return name; } \
        void set##name2(TypeTraits<type>::ParameterType a##name2) { name = a##name2; }

никого не напрягает.

если же это необходимо для самописной ORM, или XML-сериализатора, то почему бы и нет
Я пользуюсь этими макросами в нескольких своих проектах. Как только создал их — сразу почувствовал облегчение в работе. Главные аргументы я привел в статье. Это удобство и предохранение от ошибок. Член объявляется только в одном месте. Добавив или удалив член, не нужно менять расположенный в другом файле оператор сравнения. Ручное написание оператора почленного сравнения всегда имеет тот риск, что сравниваться будут не все члены. Забыть можно, проглядеть. Когда членов несколько десятков — то просто глазами трудно заметить, какой из них пропущен. А проявляться такой баг будет очень редко, как следствие — будет трудно его потом ловить.

Также я использую аналогичные макросы для автоматической генерации процедур загрузки и сохранения в файлы. Все три операции (сравнение, загрузка, сохранение) генерирует один вызов макроса. Так что, чисто по объему текста программы, получается даже экономия.
НЛО прилетело и опубликовало эту надпись здесь
А что тут дебажить? Это структура данных. Отлаживать необходимо только сами макросы (которые уже отлажены). При отладке программы, использующей такие структуры, в отладчике VisualStudio видны члены данных. Служебные члены тоже видны, но за счет префиксов в их именах, разделить одни от других несложно. Сомневаюсь, что boost::fusion::map лучше визуализируются в отладчике.

И почему «дичайший» оверхед?
Всё-таки макросы зло, код стал весьма страшным на вид!
Дичайше интересная статья.
Точно такой-же подход можно использовать, что-бы удобно и красиво реализовать проперти.

В принципе, от последнего макроса можно отказаться, если использовать не статические члены, а статические функции со статическими же переменными внутри.

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

bool testclass::operator==(testclass& comp) { int s = sizeof(*this); for (int i = 0; i < s; i+=4) { if (*(int*)((int)this + i) != *(int*)((int)&comp + i)) return false; } return true; }

В макрос завернуть не проблема.
Или я чего не знаю о хранении объектов в памяти?
bool testclass::operator==(testclass& comp)
{
    int s = sizeof(*this);
    for (int i = 0; i < s; i+=4)
    {
        if (*(int*)((int)this + i) != *(int*)((int)&comp + i)) return false;
    }
    return true;
}
Нда, не пишите так больше.
Даже не говоря о том, что будет если в структуре встретится к примеру что-нибудь типа std::string.
Это нарушает все нормы языка, и имеет кучую сайд эфектов.
Не думаю, что с std::string будет хуже, чем с double.
С double как раз все отлично будет, а со стрингом, да, неочень.
Такая реализация накладывает много ограничений и больше подходит для С (не код а принцип), но и вариант автора далеко не универсален.
Пожалуй, да. С double определённый таким образом порядок будет просто непресказуемым, а с чем-нибудь посложнее может меняться без видимого изменения данных во время программы между запусками/экземплярами.
с double будет плохо, потому что одно число в формате с плавающей точкой (мантисса-порядок) может имет разные двоичные предстваления. грубо, 3*10^6=30*10^5

но скорее глюков можно огрести на выравнивании. в структуре
struct MyClass {
     char x;
     int y;
};

между полями будет 3 байта для выравнивания, заполненных мусором.
их нужно исключить из сравнения.
Или занулять при определении структуры, что, кстати, в любом случае не помешает.
Числа с плавающей точкой хранятся в нормализованной форме. Самые маленькие в денормализованной. Эти два множества не пересекаются, так что записать их по разному можно только на бумаге.
Порядок получается нестественный.
Вы переусложнили. Итоговое решение (для пользователя) должно к такому сводиться:
struct MANPARAMS
{
    std::string name;
    int age;
    std::vector<std::string> friend_names;
    double karma;

    GENERATE_COMPARE_FUNC(name, age, friend_names, karma);
};

В предложенном Вами варианте члены структуры надо указывать в двух местах, что является потенциальным источником ошибок. Задача ставилась так, чтобы объявлять член только в одном месте.
Понимаете, вы «заставляете» каждый класс дополнительно нести кучу информации о его членах, а сама функция сравнения стала настолько сложной, что оптимизировать её компилятору уже невозможно (пройтись по динамическому массиву указателей на функции и вызвать их все — ни у какого компилятора AI не хватит).

Мой же вариант будет требовать нулевые накладные расходы: нет никаких дополнительных полей в классе, как следствие — нет дополнительных расходов на работу с ними в конструкторе/деструкторе. Кроме того, информация о типах полей известна компилятору — оператор сравнения будет сгенерирован очень эффективным для простых (базовых) типов.

Сравните то, что получается у вас для, к примеру, массива из тысячи элементов вида
struct point_type {
  int x, y;
};
— сколько ваше решение потребует выделений/освобождений памяти.

И сравните с тем, что должно быть в идеале:
struct point_type {
  int x, y;

  bool operator==(const point_type& other) {
    return x==other.x && y==other.y;
  }
};

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

Хочу еще раз обратить ваше внимание, что накладные расходы у моих макросов очень низкие:
1) Информацию о членах несет не каждый экземпляр структуры, потому что эта информация хранится в статических членах, один экземпляр на всю программу.
2) Вследствие 1), конструкторы и деструкторы моих структур не занимаются инициализацией или освобождением массивов с информацией о структуре;

Имеющиеся накладные расходы:
1) На работу оператора сравнения — вызов функций по указателям;
2) При создании каждого экземпляра структуры — на проверку флага populate_statdata столько раз, сколько в структуре членов.

Больше накладных расходов нет. Мне кажется, что это вполне неплохой компромисс по сравнению с другими решениями, которые здесь рассматривались. К тому же, если объект содержит данные не простых типов, а сложные типы, контейнеры (string, map, vector и т.д.) — то при работе операторов сравнения именно сравнение членов будет доминировать во времени выполнения, а не проход по массиву с указателями на функции.

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

Поля класса (структуры) я описывал при помощи шаблонных объектов-оболочек с общим базовым
интерфейсом (для хранения в общем списке).
При этом класс-оболочка вел себя как «умный» указатель, то есть имитировал стандартный синтаксис доступа к членам структуры.

В примитивном виде это было как-то так:
template<typename T>
class CValue : public IValue
{...};

class CMyStruct
{
public:
    CValue<int>          id;
    CValue<std::string>  name;
private:
    std::vector<IValue*> m_values;
};

Каждый объект CValue в своем конструкторе регистрирует себя в общем списке m_values.
После чего список m_values можно использовать для обхода объектов с целью сравнения или сериализации (шаблон визитор).

В итоге получилась довольно интересная штуковина.
Спасибо, тоже интересное решение. А как у вас доступ к членам производится? Какой синтаксис?
За счет того, что в классе CValue были переопределены оператор преобразования к типу шаблонного параметра, а также оператор присвоения, к переменной класса CValue можно обращаться с применением стандартного синтаксиса через оператор "." либо "->".

template<typename T>
class CValue : public IValue
{
public:
    CValue& operator=(const T& rhs)
    {
        m_value = rhs;
        return *this;
    }
    operator T()const
    {
        return m_value;
    }
};

Несколько интересней другой момент.

В моей реализации объекты класса CValue являются членами класса-структуры, которая в свою очередь содержит вектор указателей на базовый интерфейс IValue. И каждый класс CValue, при создании, должен себя регистрировать в этом векторе.
Но по законам C++ члены класса не имеют доступа к указателю на класс, в котором они содержаться.
А передавать в каждый член указатель на класс-контейнер не хотелось, т.к. каждый член пришлось бы писать дважды (при объявлении и в списке инициализации).

Этот момент я обыграл с использованием глобальных переменных-указателей, отдельных для каждого потока, что потребовало в свою очередь некоторой синхронизации:
std::map<DWORD, CStructBase*> g_mapByThreadId;
CCriticalSection              g_cs;

class IValue
{};

class CStructBase
{
public:
    CStructBase()
    {
        CScopeLock scopeLock(g_cs);
        g_mapByThreadId[GetCurrentThreadId()] = this;
    }
    static void RegisterValue(IValue* value)
    {
        CScopeLock scopeLock(g_cs);
        g_mapByThreadId[GetCurrentThreadId()]->m_values.push_back(value);
    }
private:
    std::vector<IValue*> m_values;
};

class CValue : public IValue
{
public:
    CValue()
    {
        CStructBase::RegisterValue(this);
    }
};

Делалось все это ради простоты описания структуры, которое выглядит примерно так:
class CMyStruct : public CStructBase
{
public:
    CValue<int>          id;
    CValue<std::string>  name;
};

За счет того, что вначале отрабатывает конструктор базового класса, указатель на CStructBase* сохраняется в глобальной переменной, для текущего потока. Затем поочередно конструируются члены класса CMyStruct, и каждый из них регистрирует себя в списке m_values.

В общем решение немного не стандартное, но работает, если пользоваться аккуратно.

Кстати, Ваш способ с указателями на функции мне кажется более оптимальным, т.к. позволяет хранить лишь список статических данных, что лучше соответствует природе классов.
Спасибо за разъяснения. У вас тоже интересная конструкция получилась. Надо будет на досуге попробовать объединить идеи из моей и вашей реализации. Возможно, получится сделать систему с низкими затратами времени выполнения, но не на базе макросов, а на базе шаблонов.
не нужно городить с блокировками и мапой, когда в ОС есть поддержка thread-переменных на уровне ОС без блокировок и с минимальным оверхедом

в с++
__declspec(thread) CStructBase* currentClass;

в C#, Pascal и т.п. тоже есть аналогичные windows-specific кейворды ([ThreadStatic], threadvar)
TLS конечно интересная вещь, но боюсь, что в ней много подводных камней, например только что нашел статью на RSDN:
LoadLibrary и __declspec(thread)
любопытный факт, не знал
Вот еще интересную статью нашел: TLS изнутри.
Оказывается есть две различные реализации TLS — динамическая и статическая.

А вообще считаю, что прежде чем использовать любую вещь в многопоточной среде, необходимо досконально разобраться как эта вещь устроена.
да, и ещё проблема этого кода — создание CValue вне наследника CStructBase. тогда этот CValue допишется в последний созданный экземпляр CStructBase (который может быть уже разрушен).

по-хорошему, после завершения всех конструкторов членов CMyStruct надо сделать g_mapByThreadId[GetCurrentThreadId()] = NULL

что-то не соображу, как это сделать автоматически, чтобы не повторять в каждом конструкторе CMyStruct
А смысл, защита от дурака? В любом случае дурак напишет глючную программу.
В оригинальном коде класс CValue был объявлен как вложенный protected класс.
я люблю assertions и пишу их часто.
наверное, я дурак ))
А я не люблю асерты, наверное я тоже дурак :)

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

В общем все зависит от специфики конкретных проектов.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации