Саморегистрирующаяся фабрика на основе шаблонов с современной сборкой CMake и обнаружением добавления исходного кода

Я пытаюсь создать структуру проекта, отвечающую следующим целям:

  • приложению необходимо создать экземпляры классов с помощью фабрики;
  • классы, экземпляры которых созданы фабрикой, должны иметь возможность самостоятельно регистрироваться в фабрике;
  • Использование системы саморегистрации должно быть очень простым и надежным.

Эта часть проблемы прекрасно решается в сообщении в блоге Нира Фридмана по теме (хотя я получаю предупреждения о компиляции, когда использую CLang, но это уже другая история).

Теперь все становится сложнее, когда я пытаюсь применить это в контексте проекта, в котором люди хотели бы добавлять классы, просто добавляя источники. Основываясь на примере Нира (из которого я удалил несколько вещей для удобства), я создал следующий макет (на основе сообщение Рафаэля Вараго) [см. репозиторий GitHub]:

.
├── CMakeLists.txt
├── app
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
└── libs
    ├── CMakeLists.txt
    ├── libanimal
    │   ├── CMakeLists.txt
    │   ├── include
    │   │   └── animal
    │   │       └── Animal.h
    │   └── src
    │       ├── Cat.cpp
    │       └── Dog.cpp
    └── libfactory
        ├── CMakeLists.txt
        └── include
            └── factory
                └── Factory.h

Когда я писал CMakeLists.txt файлы, я пытался применить современные методы CMake, за одним исключением, которое будет подробно описано ниже.

Каталог app содержит код приложения, вызывающего фабрику:

#include <animal/Animal.h>

int main() {
    auto x = Animal::make("Dog", 3);
    auto y = Animal::make("Cat", 2);
    x->makeNoise();
    y->makeNoise();
    return 0;
}

Каталог libs содержит два подкаталога:

  • libfactory содержит код шаблона фабрики и построен как каталог только для заголовков;
  • libanimal содержит абстрактный класс Animal и связанную с ним фабрику, а также код для дочерних классов; он построен как статическая библиотека с зависимостью от libfactory.

Я хочу, чтобы libanimal имел своего рода поведение «библиотеки плагинов времени компиляции»: потомки класса Animal будут самостоятельно регистрироваться на Animal factory после компиляции. Этой цели должным образом служит (по крайней мере, на бумаге) метод Нира (Animal.h):

#pragma once

#include <factory/Factory.h>

struct Animal : Factory<Animal, int> {
    Animal(Key) {}
    virtual void makeNoise() = 0;
};

Теперь я хочу объединить это с возможностью централизовать дочерний код в одном файле cpp, который CMake автоматически обнаруживает при сборке проекта. Преимущество этого заключается в том, что он позволяет очень легко добавлять и удалять функции (просто вставьте новый файл или удалите его). Для этой цели я использовал глобус в libanimal CMakeLists.txt, тем самым нарушая современные передовые практики CMake. Если есть лучший способ добиться этого, я, конечно, буду рад его реализовать. Код для Dog.cpp:

#include <iostream>
#include <animal/Animal.h>

class Dog : public Animal::Registrar<Dog> {
  public:
    Dog(int x) : m_x(x) {}

    void makeNoise() override { std::cerr << "Dog: " << m_x << "\n"; }

  private:
    int m_x;
};

Когда я создаю проект, кажется, что все идет нормально, за исключением предупреждений, которые я получаю также при компиляции проекта Nir (я получаю их с помощью clang, но не с помощью gcc):

In file included from /Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/app/src/main.cpp:3:
In file included from /Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/libs/libanimal/include/animal/Animal.h:4:
In file included from /Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/libs/libfactory/include/factory/Factory.h:4:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string:505:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string_view:176:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/__string:57:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/algorithm:644:
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2339:5: warning: delete called on 'Animal' that is abstract but has non-virtual destructor [-Wdelete-abstract-non-virtual-dtor]
    delete __ptr;
    ^
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2652:7: note: in instantiation of member function 'std::__1::default_delete<Animal>::operator()' requested here
      __ptr_.second()(__tmp);
      ^
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2606:19: note: in instantiation of member function 'std::__1::unique_ptr<Animal, std::__1::default_delete<Animal> >::reset' requested here
  ~unique_ptr() { reset(); }
                  ^
/Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/app/src/main.cpp:6:14: note: in instantiation of member function 'std::__1::unique_ptr<Animal, std::__1::default_delete<Animal> >::~unique_ptr' requested here
    auto x = Animal::make("Dog", 3);
             ^
1 warning generated.

Однако когда я запускаю приложение, я получаю следующую ошибку:

  • версия clang:
/Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/build/app/app
libc++abi.dylib: terminating with uncaught exception of type std::out_of_range: unordered_map::at: key not found
  • версия gcc:
/Users/vincent/Documents/src/personal/sandboxes/cpp_factory_split/cmake-build-release-gcc/app/app
terminate called after throwing an instance of 'std::out_of_range'
  what():  _Map_base::at

Похоже, это означает, что таблица фабрики пуста, и я не понимаю почему.

Вопросов

  1. Я неправильно понял замысел Нира?
  2. Если да на 1., знает ли кто-нибудь о дизайне с саморегистрацией, который потребует столь же минимального обслуживания, как этот, и будет подходить для моего варианта использования?
  3. Если нет на 1., что я делаю не так?

person M4urice    schedule 13.06.2019    source источник
comment
вам нужен виртуальный деструктор для Animal. Обязательно при определении интерфейса на C ++. Обратите внимание, что этот dtor был добавлен в конец блога Нира. В качестве альтернативы вы можете добавить его в шаблон Factory.   -  person Marek R    schedule 13.06.2019


Ответы (2)


Сообщение Нира Фридмана неверно. Все трюки с саморегистрацией, которые я видел, работают скорее «случайно» (другими словами, ваш компилятор дал вам бесплатный проход), а не по языковому правилу.

C ++ будет поддерживать порядок создания глобальных объектов только для каждого модуля компиляции. Поэтому, если у вас есть основной файл и какой-то класс в другом файле, глобальные переменные из другого файла могут быть инициализированы в первой точке, где используется «что-нибудь» из другого файла. Обычно глобальные переменные все еще инициализируются перед основным, потому что компилятор не может понять, насколько правильно задерживать (и в любом случае нет особых причин для задержки статической инициализации), но стандарт не гарантирует этого. Это работает только потому, что компилятор на самом деле несколько ленив.

Вдобавок к этому он использует demangle(typeid(T).name()). Это тоже неверно - стандарт не гарантирует наличие действительного содержимого для typeid(T).name() вызова. Может быть, пусто. Его также можно удалить (когда-либо видели параметр -no-rtti для gcc? Хотя, по крайней мере, вы можете вручную удалить его самостоятельно).

Не используйте такие уловки, если вы не хотите придерживаться конкретного компилятора и его конкретной версии. Эти уловки - в лучшем случае - определены реализацией.

РЕДАКТИРОВАТЬ: «правильный» (другими словами, соответствующий стандарту) способ решения - это своего рода макрос и парсер, который будет анализировать ваши файлы, искать все классы, у которых есть какой-то специальный базовый класс. Затем этот инструмент записывает функцию инициализации, которую вы вручную вызываете из основного, и все в порядке. Например, вы можете использовать clang (сканировать весь проект на предмет классов и их базовых классов и быстро создавать такую ​​функцию).

person Radosław Cybulski    schedule 13.06.2019

Проблема - bool Factory<Base, Args...>::Registrar<T>::registered
Обратите внимание, что на это значение ссылаются только тогда, когда оно инициализируется:

template <class Base, class... Args>
template <class T>
bool Factory<Base, Args...>::Registrar<T>::registered =
    Factory<Base, Args...>::Registrar<T>::registerT();

Теперь, когда это значение не используется, оптимизатор кода удалил его. Поскольку он был удален, он не инициализируется. Поскольку это не было инициализировано, процесс регистрации не производился.

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

Вы должны сделать что-то, что предотвратит удаление bool Factory<Base, Args...>::Registrar<T>::registered оптимизатором.

Чтобы доказать свою точку зрения, я создал ваш проект на github на Mac OS. Я запустил этот сценарий:

nm app/Debug/app | awk '{print $NF}' | while read sym
do
    c++filt $sym | grep "Factory"
done

Это выводит только то, что:

guard variable for Factory<Animal, int>::data()::s
Factory<Animal, int>::data()
std::__1::unique_ptr<Animal, std::__1::default_delete<Animal> > Factory<Animal, int>::make<int>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int&&)
Factory<Animal, int>::data()::s

Обратите внимание, что нет registered статических полей, Cat или Dog. Все это было удалено компоновщиком.

Пояснение к сценарию

  • nm app/Debug/app печатает все символы для app
  • awk '{print $NF}' фильтровать последние столбцы (что дает искаженные имена)
  • while read sym перебирать искаженные имена
  • c++filt $sym разобрать имена
  • grep "Factory" показывать только вещи, относящиеся к Factory.

Теперь, когда в cat.cpp я добавил это:

void dummy()
{
    std::cout << Animal::Registrar<Cat>::registered << '\n';
}

И вызвал его в main, создание «Кошки» работает («Собака» продолжает выходить из строя).

Сценарий после этого печатает:

Factory<Animal, int>::Registrar<Cat>::registered
Factory<Animal, int>::Registrar<Cat>::registerT()
Factory<Animal, int>::Registrar<Cat>::Registrar()
Factory<Animal, int>::Registrar<Cat>::~Registrar()
Factory<Animal, int>::Registrar<Cat>::~Registrar()
Factory<Animal, int>::Registrar<Cat>::~Registrar()
typeinfo for Factory<Animal, int>
typeinfo for Factory<Animal, int>::Registrar<Cat>
typeinfo name for Factory<Animal, int>
typeinfo name for Factory<Animal, int>::Registrar<Cat>
vtable for Factory<Animal, int>::Registrar<Cat>
Factory<Animal, int>::data()::s
Factory<Animal, int>::Registrar<Cat>::registerT()::'lambda'(int)::operator()(int) const
Factory<Animal, int>::Registrar<Cat>::registerT()::'lambda'(int)::operator std::__1::unique_ptr<Animal, std::__1::default_delete<Animal> > (*)(int)() const
Factory<Animal, int>::Registrar<Cat>::registerT()::'lambda'(int)::__invoke(int)

Что окончательно докажет, что я прав. Компоновщик заметил, что символы, создающие экземпляр шаблона Factory<Base, Args...>::Registrar<T>::registered, недоступны из main (существует только циклическая зависимость), поэтому он был удален.

Здесь вы можете найти ответ, как подойти к этой проблеме в gcc (это не работает в clang - это атрибуты и флаги компоновщика отсутствуют в clang), но, как видите, это довольно сложно.

person Marek R    schedule 13.06.2019