#c #qt #macros #c-preprocessor
#c #qt #макросы #c-препроцессор
Вопрос:
В моих приложениях Qt я использую макрос препроцессора для автоматического объявления и регистрации мета-типа:
#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE)
Q_DECLARE_METATYPE(TYPE)
static struct TYPE ## _metatype_registrar {
TYPE ## _metatype_registrar() {
qRegisterMetaType<TYPE>();
}
} _ ## TYPE ## _metatype_registrar;
Обычно я использую его в заголовочном файле после определения типа, который я хочу использовать в системе метатипов Qt, например:
struct MyDataType {
int foo;
double bar;
};
Q_DECLARE_AND_REGISTER_METATYPE(MyDataType)
Это работает, потому что статический экземпляр struct, который он определяет, всегда будет иметь свой конструктор, вызываемый при запуске программы. Я предпочитаю этот подход, потому что теперь мне не нужно отдельно регистрировать каждый тип, о котором я забочусь, где-то в моем коде в начале программы. (Раньше я использовал этот подход, и я часто забывал добавлять туда новые типы, потому что я забывчивый, что приводит к досадным ошибкам во время выполнения.)
Единственная проблема с использованием этого макроса заключается в том, что тип будет зарегистрирован один раз для каждого исходного файла, который включает соответствующее использование Q_DECLARE_AND_REGISTER_METATYPE
.
Технически Qt позволяет вызывать qRegisterMetaType
несколько раз для одного и того же типа и игнорирует последующие вызовы. Так что это не приводит к каким-либо ошибкам. Но это по-прежнему кажется мне неэффективным и нечистым. Каждый исходный файл, который содержит заголовок — прямо или косвенно — который использует Q_DECLARE_AND_REGISTER_METATYPE
, расширит его до кода, определяющего отдельную статическую структуру, конструктор которой будет выполняться при запуске, засоряя скомпилированный код множеством избыточных структур и функций, все из которых будут выполняться.
Я хотел бы посмотреть, смогу ли я улучшить его, чтобы каждое использование Q_DECLARE_AND_REGISTER_METATYPE
для определенного типа приводило к тому, что он выполнял только код, который он расширяет до одного раза для каждого типа во всей программе, и (как что-то вроде цели «растяжения») не создавал избыточных структур, предполагая, что это возможно.
Более конкретно, если файл заголовка foo.h
содержит строку Q_DECLARE_AND_REGISTER_METATYPE(Foo)
, и файлы a.cpp
, b.cpp
, и c.cpp
все включают foo.h
, я хочу, чтобы скомпилированная программа вызывалась только qRegisterMetaType<Foo>();
один раз. И, если возможно, я хочу, чтобы он не создавал несколько избыточных экземпляров _Foo_metatype_registrar
.
Если бы не соображения эффективности и чистоты, я бы хотел это сделать, возможно, я смогу изучить некоторые интересные приемы предварительной компиляции C !
Есть ли какой-либо способ сделать это?
Комментарии:
1. Посмотрите, что Qt заставил вас сделать. Это не красиво.
2. Возможно, вы могли бы использовать
std::call_once
?
Ответ №1:
Это та проблема, которая может быть решена с помощью одноэлементного шаблона. В двух словах, шаблон singleton гарантирует, что когда-либо будет создан только один экземпляр типа.
Одним из подходов может быть создание внутреннего одноэлементного класса, который реализует ваше регистрационное действие в конструкторе. Затем ваш внешний класс получит экземпляр singleton в своем конструкторе. Независимо от того, сколько static
внешних классов создано, внутренний класс является одноэлементным, поэтому его конструктор будет вызван только один раз. Собрав все это вместе, ваш макрос может быть переписан следующим образом:
#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE)
Q_DECLARE_METATYPE(TYPE)
static struct TYPE ## _metatype_registrar {
class inner {
inner() { qRegisterMetaType<TYPE>(); }
inner(const inner amp;) = delete;
void operator=(const inner amp;) = delete;
public:
static inner amp; once () {
static inner instance;
return instance;
}
};
TYPE ## _metatype_registrar() {
inner::once();
}
} _ ## TYPE ## _metatype_registrar;
Я провел расследование std::call_once
, и оно работает.
#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE)
Q_DECLARE_METATYPE(TYPE)
static struct TYPE ## _metatype_registrar {
TYPE ## _metatype_registrar() {
static std::once_flag f;
std::call_once(f, qRegisterMetaType<TYPE>);
}
} _ ## TYPE ## _metatype_registrar;
Однако для этого требуется, чтобы вы #include <mutex>
и связались с вашей библиотекой потоков, что может показаться странным, если ваша программа не является многопоточной.
Обратите внимание, это предотвратит возникновение нескольких регистраций. Однако ваш метод решения требует, чтобы статические экземпляры были определены в единицах перевода заголовочными файлами, потому что в противном случае вы нарушили бы правило единого определения. Таким образом, приведенные выше решения не решают проблему того, что ваш код завален этими избыточными статическими объектами.
Однако есть способ обойти это, если вы используете шаблоны. C имеет особый случай для шаблонных классов, где с его помощью избыточные определения статических элементов автоматически объединяются в одно определение. Это связано с тем, что шаблоны полностью реализованы в заголовочных файлах, где также обычно определяются его статические члены. Итак, вы могли бы создать общий шаблонный класс для вашего регистратора.
template <typename T>
class MetatypeRegistrar {
MetatypeRegistrar () { qRegisterMetatype<T>(); }
public:
static MetatypeRegistrar _registrar;
};
template <typename T>
MetatypeRegistrar<T> MetatypeRegistrar<T>::_registrar;
Обратите внимание, что последние две строки предоставляют определение шаблона для объявления статического члена _registrar
.
И теперь вы можете выполнить автоматическую регистрацию в вашем заголовочном файле для вашего типа с помощью этого макроса:
#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE)
Q_DECLARE_METATYPE(TYPE)
template class MetatypeRegistrar<TYPE>;
По сути, это явное создание экземпляра шаблона с указанным типом.
Несколько исходных файлов могут создавать несколько экземпляров шаблона для того же MyDataType
, что и файл заголовка. включен. Однако компилятор автоматически объединит подразумеваемые несколько экземпляров элемента данных статического шаблонного класса в один экземпляр.
Комментарии:
1. Это довольно умно! Я не рассматривал возможность использования внутреннего одноэлементного класса. Я нашел еще одно возможное решение, которое заключается в изменении исходного макроса для включения статического bool, который при вызове получает значение true
qRegisterMetaType
, а затем предотвращает его вызов, если оно уже true. Что-то вродеstatic bool needInit = true; if (needInit) { qRegisterMetaType<TYPE>(); needInit = false; }
. Похоже, это имеет те же преимущества и недостатки: для этого требуется одна статическая переменная,qRegisterMetaType
вызывается один раз, и она по-прежнему засоряет приложение избыточными структурами.2. Я начинаю думать, что избавление от избыточных структур является более важной целью этого вопроса, чем я думал, потому что просмотр ассемблера, созданного для моего проекта, показывает тонны ненужных глобальных переменных, все из которых инициализируются при запуске. Тем не менее, я начинаю сомневаться, что есть какой-либо способ предотвратить это, если не использовать какой-то пользовательский шаг препроцессора перед компиляцией. (Учитывая, что я уже использую его с Qt, очень жаль, что они не предлагают автоматическую регистрацию типов в качестве функции!)
3. @GuyGizmo: статический флаг можно использовать, но он подвержен ошибкам, если вы когда-либо переходите на многопоточный. Итак, вам понадобился бы какой-то способ пометить проблему, которую нужно исправить, если бы это было так. Одноэлементная идиома C будет потокобезопасной в соответствии со спецификацией языка.
4. @GuyGizmo: Но, да, мой последний абзац должен был предупредить вас, что ваше желание выполнить регистрацию в заголовочном файле делает необходимым создание этих избыточных объектов. Вы можете рассмотреть флаги условной компиляции для выборочного включения / отключения генерации регистрационного кода. По умолчанию регистрационный код не генерируется. Затем в специальный исходный файл вы включаете все заголовочные файлы, которым требуется код регистрации, и меняете флаги, чтобы сгенерировать код регистрации.
5. @jhx Да, я рассматривал это, но это, по сути, то же самое, что и мой первоначальный подход, который заключался в том, чтобы просто иметь где-то в моем коде функцию, которая явно вызывает
qRegisterMetaType
каждый тип, который в ней нуждается. Если я собираюсь сделать что-то не автоматическое, я могу также упростить это и сделать это. Причина, по которой я пытаюсь избежать этого, заключается в том, что я забывчивый болван и постоянно забываю добавлять типы к этой функции, что приводит к ошибкам во время выполнения, которые иногда было трудно отследить. Итак, я ищу умный автоматический метод, который не создает слишком много мусора.