Саморегистрационная фабрика на основе шаблонов с современной сборкой CMake и распознаванием исходного кода - PullRequest
0 голосов
/ 13 июня 2019

Я пытаюсь настроить структуру проекта для достижения следующих целей:

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

Эта часть проблемы хорошо решена с помощью сообщения в блоге Нира Фридмана на эту тему (хотя я получаю предупреждения о компиляции при использовании 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

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

Каталог 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 после компиляции. Эта цель выполняется должным образом (по крайней мере на бумаге) по методу Нира (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.

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

  • лязг версия:
/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. Что я делаю не так?

Ответы [ 2 ]

0 голосов
/ 13 июня 2019

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

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

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

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

EDIT: «Правильный» (другими словами, соответствующий стандарту) способ решения этой проблемы - это своего рода макрос и синтаксический анализатор, который будет анализировать ваши файлы, искать все классы, которые имеют некоторый специальный базовый класс. Затем этот инструмент записывает функцию инициализации, которую вы вызываете вручную из main, и это все хорошо. Например, для этого вы можете использовать clang (отсканировать весь проект на наличие классов и их базовых классов и быстро создать такую ​​функцию).

0 голосов
/ 13 июня 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" показывать только вещи, связанные с фабрикой.

Теперь, когда в 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), но, как вы можете видеть, это довольно сложно.

...