基于模板的自注册工厂,具有现代 CMake 构建和源插入检测



>我正在尝试建立一个项目结构,以实现以下目标:

  • 应用程序需要使用工厂实例化类;
  • 工厂
  • 实例化的类必须能够自行注册到工厂;
  • 自助注册系统的使用应该非常简单和万无一失。

Nir Friedman关于该主题的博客文章很好地解决了这部分问题(尽管我在使用CLang时收到编译警告,但那是另一回事了)。

现在,当我尝试将其应用于人们希望通过简单地放入源代码来添加类的项目上下文中时,事情变得更加棘手。在 Nir 的示例(为了方便起见,我从中剥离了一些东西)的基础上,我创建了以下布局(基于 Rafael Varago 的帖子)[参见 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工厂。Nir的方法(Animal.h)可以适当地达到这个目的(至少在纸面上):

#pragma once
#include <factory/Factory.h>
struct Animal : Factory<Animal, int> {
Animal(Key) {}
virtual void makeNoise() = 0;
};

现在,我想将其与将子代码集中在单个 cpp 文件中的能力相结合,该文件在构建项目时由 CMake 自动检测。这样做的优点是它允许非常轻松地添加和删除功能(只需放入新文件或删除它)。为此,我在libanimalCMakeLists.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
  • 海湾合作委员会版本:
/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 否,我做错了什么?

问题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

为了证明我的观点,我在Mac OS上构建了你的github项目。我运行了此脚本:

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静态字段或CatDog。所有这些都被链接器删除了。

脚本说明

  • 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正在工作("狗"不断失败)。

之后打印的 Scrip 打印:

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)

最终证明我是对的。Linker 注意到无法从main访问实例化Factory<Base, Args...>::Registrar<T>::registered模板的符号(只有循环依赖项),因此将其删除。

在这里你可以找到如何在 gcc 中解决这个问题的答案(这在 clang 中不起作用 - 这个属性和链接器标志在 clang 中不存在),但正如你所看到的,它非常棘手。

Nir Friedman的帖子是错误的。我见过的所有自注册技巧都更多地是"偶然"(换句话说,你的编译器给了你一个免费通行证),而不是语言规则。

C++将仅保留每个编译模块实例化全局的顺序。因此,如果你在其他文件中有主文件和某个类,那么来自其他文件的全局变量可能会在使用来自其他文件的"任何东西"的第一点被初始化。通常全局变量仍然在main之前初始化,因为编译器无法弄清楚延迟的正确程度(无论如何都没有理由延迟静态初始化),但标准不能保证这一点。它之所以有效,只是因为编译器实际上有些懒惰。

最重要的是,他正在使用demangle(typeid(T).name()).这也是错误的 - 标准不保证typeid(T).name()通话的任何有效内容。它可能为空。它也可能被删除(见过 -no-rtti 选项 gcc 吗?尽管至少你可以自己手动删除它)。

不要使用这种技巧,除非你愿意坚持使用具体的编译器,而且它是具体的版本。这些技巧充其量是定义的实现。

编辑: 解决这个问题的"适当"(换句话说,符合标准)的方法是某种宏和解析器,它将解析您的文件,查找具有一些特殊基类的所有类。然后这个工具编写初始化函数,你从main手动调用它,一切都很好。例如,您可以使用 clang (扫描整个项目以查找类及其基类并快速构建此类函数)。

最新更新