LINUX.ORG.RU

Статическая инициализация классов в C++

 , ,


0

1

Передо мной встала задача сделать некие манипуляции с конкретным классом (классами) до создания первого объекта.

Рассмотрим частный случай необходимости статической инициализации классов.

Как известно, в Qt для того, чтобы можно было передавать пользовательские типы между сигнал-слотами при Qt::QueuedConnection, необходимо предварительно зарегистрировать данный тип в мета-объектной системе Qt с помощью qRegisterMetaType<Foo>(«Foo»).

В одном из проектов есть базовый класс BaseEvent и несколько десятков наследуемых (например, EventFoo, EventBar). Мне нужно сделать регистрацию типа до вызова любого из сигналов, которые передают объекты данных классов между потоками.

Почему не регистрировать тип непосредственно перед созданием connect или в конструкторе класса, который создает connect'ы? Потому что типов много, как я написал выше, несколько десятков. Если забыть зарегистрировать тип, то об этом я узнаю только в процессе работы, так как наличие зарегистрированного типа проверяется в runtime.

Найденные решения:

1. Регистрировать в конструкторе [плохо]

class BaseEvent
{
public:
    explicit BaseEvent() { qRegisterMetaType<BaseEvent>("BaseEvent"); }
}


class EventFoo : public BaseEvent
{
public:
    explicit EventFoo() { qRegisterMetaType<EventFoo>("EventFoo"); }
}

class EventBar : public BaseEvent
{
public:
    explicit EventBar() { qRegisterMetaType<EventBar>("EventBar"); }
}

class 
Недостатки очевидны: накладные расходы в runtime.

2. Статический member с конструктором-инициализатором

class BaseEvent
{
public:
    explicit BaseEvent() {}
private:
    class Initializer
    {
    public:
        explicit Initializer() { qRegisterMetaType<BaseEvent>("BaseEvent"); }
    } static const _init;
}
 

class EventFoo : public BaseEvent
{
public:   
    explicit EventFoo() { }

private:
    class Initializer
    {
    public:
        explicit Initializer() { qRegisterMetaType<EventFoo>("EventFoo"); }
    } static const _init;
}
 

class EventBar : public BaseEvent
{
public:
    explicit EventBar() { }

private:
    class Initializer
    {
    public:
        explicit Initializer() { qRegisterMetaType<EventBar>("EventBar"); }
    } static const _init;

}

Преимущества: минимальные накладные расходы по памяти (один int на весь класс), отсутствие накладных расходов при создании объекта

Недостатки: много дублирующего кода, так и хочется как-то заменить на шаблоны и/или макросы.

Что посоветуете?

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

class EventFoo : public BaseEvent
{
public:
    explicit EventFoo() { }

private:
    static const Initializer<EventFoo> _init;
}

Или хотя бы на макросах:

class EventFoo : public BaseEvent
{
public:
    explicit EventFoo() { }
 
private:
    EVENT_INIT(EventFoo)
}

★★★★★

Последнее исправление: Chaser_Andrey (всего исправлений: 4)

сделай BaseEvent шаблонным и передавай тип параметром шаблона, а имя - параметром конструктора (строку шаблоном передавать муторно совсем).

меня больше кумарит что при этом придется делать инициализацию статика в .cpp, но се-ля-ви.

dib2 ★★★★★
()

А в чём вопрос? Решение у тебя уже есть.

Для шаблонов можно использовать typeid, для макросов даже такой проблемы нет.

Но вообще это принято делать в main().

E ★★★
()

В одном из проектов есть базовый класс BaseEvent и несколько десятков наследуемых

Передавай указатель и пользуйся полиморфизмом.

E ★★★
()

Just for fun, родилось нечто страшное, но рабочее:

#include <cstdlib>

#include <iostream>
#include <typeinfo>

#include <cxxabi.h>

class AutoRegister
{
    class InitializerPrivate
    {
    };

public:
    explicit AutoRegister(const InitializerPrivate&)
    {
    }

    template <class Child>
    class Initializer : public InitializerPrivate
    {
        class StaticInit
        {
        public:
            StaticInit()
            {
                int status;
                char *childName = abi::__cxa_demangle(typeid(Child).name(), 0, 0, &status);
                std::cout << "Registering child: " << childName << std::endl;
                // XXX: qRegisterMetaType<EventFoo>("EventFoo");
                std::free(childName);
            }

            void nop() const
            {

            }
        };

        static const StaticInit _init;

    public:
        explicit Initializer()
        {
            _init.nop();
        }
    };
};

class BaseEvent : protected virtual AutoRegister
{
public:
    explicit BaseEvent() : AutoRegister(Initializer<BaseEvent>())
    {
    }
};

class EventFoo : public BaseEvent
{
public:
    explicit EventFoo() : AutoRegister(Initializer<EventFoo>())
    {
    }
};

class EventBar : public BaseEvent
{
public:
    explicit EventBar() : AutoRegister(Initializer<EventBar>())
    {
    }
};

template<class T>
const typename AutoRegister::Initializer<T>::StaticInit AutoRegister::Initializer<T>::_init = typename AutoRegister::Initializer<T>::StaticInit();

int main(void)
{
    return 0;
}

Плюсы:

  • Виртуальное наследование не даст забыть о необходимости инициализации.
  • Нет необходимости писать литерал с именем класса.

Минусы:

  • Деманглинг во время исполнения, хотя только один раз.
  • Сообщения об ошибках в этом коде ещё нужно суметь понять.
  • Вероятно, требует доработки (возможно, нетривиальной).
xaizek ★★★★★
()
Ответ на: комментарий от E

Но вообще это принято делать в main().

Это библиотека. Такие вещи хочется спрятать от юзера.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от xaizek

Спасибо! Идею понял, на первый взгляд — пока лучшее решение.

Заодно смотрю здесь https://stackoverflow.com/questions/281818/unmangling-the-result-of-stdtype-i...

Деманглинг во время исполнения, хотя только один раз.

В смысле, плохо, что будет однократный оверхед в рантайме? Или у деманглинга есть другие недостатки?

abi::__cxa_demangle

Оно gcc-only или кросс-компиляторное? Смущает, что по вышеуказанной ссылке пример с __cxa_demangle заключен в #ifdef __GNUG__.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от Chaser_Andrey

Защита от человеческого фактора.

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

MikeDM ★★★★★
()
Ответ на: комментарий от Chaser_Andrey

В смысле, плохо, что будет однократный оверхед в рантайме? Или у деманглинга есть другие недостатки?

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

Оно gcc-only или кросс-компиляторное?

GCC, clang скорее всего тоже нечто подобное содержит, VS сама по себе выдаёт читаемые имена. Манглинг всегда зависит от компилятора.

И зачем void StaticInit::nop()?

Иначе объект объявляется, но не определяется, из-за того что один из родительских классов — шаблон. Думается, должно существовать более красивое решение, гарантирующее генерацию статического поля.

xaizek ★★★★★
()
Ответ на: комментарий от MikeDM

Если из нескольких десятков классов забыть зарегистрировать хотя бы один (после добавления/изменения новых классов, после рефакторинга и т.д.), то об этом можно будеть узнать только в runtime (когда будет послан сигнал с объектом, для которого забыли зарегистрировать тип в мета-объектной системе Qt).

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

Поэтому, если компилятор может меня предупреждать и бить по рукам — пусть он это делает.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от DELIRIUM

У каждого наследуемого класса свой набор геттеров, сеттеров и просто методов. Общего интерфейса у них не может быть by design.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от Chaser_Andrey

Бестолковое занятие. Не стоит в погоне за уберзащищенностью от программиста-дурака усложнять код и делать всякую акробатику в ущерб общей гибкости. Лучшее средство здесь - вменяемая документация с четко обозначеными «нельзя» и «можно», и неиллюзорная вероятность получить по носу накосячившим

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

Документация не спасает от простого человеческого фактора.

Для чего придумали статические анализаторы кода и юнит-тесты?

Chaser_Andrey ★★★★★
() автор топика

Хм, а если зайти с другого конца. Когда пишешь новый класс, заполняешь статическую структуру с инфой (через макрос например), а инит функция библиотеки уже делает все, что надо с этими классами. Реализация вроде Q_DECLARE_TYPEINFO_BODY или как GoogleTest собирает инфу о все юнит тестах.

Rubbiroid
()
Ответ на: комментарий от yoghurt

Чем это решение лучше юнит-тестов? Кроме того, внезапно, юнит-тесты не страхуют от случайного забывания добавления теста для нового класса.

А вышеприведенный метод гарантирует, что инициализация будет проведена, иначе проект вообще не скомпилируется.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от Chaser_Andrey

Чем это решение хуже юнит-тестов?

fixed :)

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от Chaser_Andrey

юнит-тесты не страхуют от случайного забывания

Ну ё

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

Лучшая документация «можно»/«нельзя» — это стандарт языка и код, написанный в соответствии с этим стандартом.

На счёт юнит-теста... Ну смотри, в данном случае придется написать в юнит-тесте более 50 строк (пусть по одной строке на один класс), оформить подпроект, и держать в актуальном состоянии придется уже два места, одно в проекте, другое в юнит-тесте. И всё равно это не дает гарантий от невнимательности.

Зачем перекладывать с больной головы на здоровую? Лично я лучше добавлю небольшой хук, задокументирую его, и отдам все проверки компилятору.

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

Chaser_Andrey ★★★★★
() автор топика

делай по первому варианту, так надёжнее. И забей на рантайм, ещё не известно, как оно быстрее будет... IRL вопрос далеко не так очевиден, как оно тебе кажется.

emulek
()
Ответ на: комментарий от yoghurt

не останавливай, может заблудится

anonymous
()

Если второй вариант устраивает, то запилите макрос для него и все:

#define EVENT_INIT(N) \
class Initializer \
    { \
    public:\
        explicit Initializer() { qRegisterMetaType<N> (#N); } \
    } static const _init; 

Самое простое и прямое... только что бы после \ пробелов не було;-)

AIv ★★★★★
()
Ответ на: комментарий от emulek

qRegisterMetaType — не очень дешевая функция, а, в моем случае, объекты унаследованных от BaseEvent создаются сотнями и тысячами в секунду. Поэтому нужно минимум лишних расходов в runtime.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от ilammy

Один раз в самом начале — не страшно. Потом приложение работает 24/7, и главное, поменьше расходов уже после старта.

Chaser_Andrey ★★★★★
() автор топика
Ответ на: комментарий от asaw

Причем тут абсурд? Вполне рабочее решение.

AIv ★★★★★
()

Что посоветуете?

не любить себе мозг :)

макросы, в данном случае, - вполне себе Qt way

#include <QtCore/QCoreApplication>
#include <QtCore/QMetaType>
#include <QtCore/QDebug>

#define Q_REGISTER_METATYPE(x) \
struct Receptionist ##x \
{ \
    explicit Receptionist ##x () { qRegisterMetaType<x>(#x); } \
} static const receptionist ## x;

class BaseEvent
{
public:
    explicit BaseEvent() { qRegisterMetaType<BaseEvent>(); }
};
Q_DECLARE_METATYPE(BaseEvent)

class EventFoo : public BaseEvent
{
public:
    explicit EventFoo() {}
};
Q_DECLARE_METATYPE(EventFoo)
Q_REGISTER_METATYPE(EventFoo)

class EventBar : public BaseEvent
{
public:
    explicit EventBar() {}
};
Q_DECLARE_METATYPE(EventBar)

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    const int baseEventType = QMetaType::type("BaseEvent");
    qDebug() << "BaseEvent:" << baseEventType << QMetaType::typeName(baseEventType);
    const int eventFooType = QMetaType::type("EventFoo");
    qDebug() << "EventFoo:" << eventFooType << QMetaType::typeName(eventFooType);
    const int eventBarType = QMetaType::type("EventBar");
    qDebug() << "EventBar:" << eventBarType << QMetaType::typeName(eventBarType);
    return a.exec();
}
BaseEvent: 0 void 
EventFoo: 256 EventFoo 
EventBar: 0 void

// by the way, надо бы фич-реквест запостить в кути

Stil ★★★★★
()
Ответ на: комментарий от Stil

Как минимум - отладка такого говнокода. Макросы - хорошая вещь в ассемблере или в C, но в C++ их следует запретить.

asaw ★★★★★
()
Ответ на: комментарий от Chaser_Andrey

По крайней мере она поддерживается средствами языка.

asaw ★★★★★
()
Ответ на: комментарий от asaw

Как минимум - отладка такого говнокода.

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

хотя сейчас - да, говнокод

Макросы - хорошая вещь в ассемблере или в C, но в C++ их следует запретить.

ну а что ты предлагаешь? тот БДСМ с деманглингом выше по треду?

Stil ★★★★★
()
Ответ на: комментарий от asaw

Эх, молодежжжь... некотрые вещи в плюсах делаются только на макросах.

AIv ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.