1

在我正在编写的图书馆中,我有一堂课:

template<typename T>
class my_class {};

和一个功能:

template<typename T>
void my_function(...) {};

我想保证我的代码的用户my_function<T>在尝试实例化my_class<T>. 换句话说,如果他们尝试实例化my_class模板而不实例化相应的my_function模板,我想生成一个编译错误。

例子:

int main() {
    my_func<int>()      // cause instantiation of my_func<int>

    my_class<int> foo;  // okay, because my_func<int> exists
    my_class<char> bar; // compile error! my_func<char> does not exist
}

请注意,my_class<T>不需要在my_func<T>内部使用,因此实例化它不会自动导致my_func<T>实例化。

编辑[0]:​​我不能给my_func<T>自己打电话,因为我需要用户调用它并传递告诉库如何处理的信息my_class<T>。IE。如果他们想使用 a my_class<char>,他们需要在代码中的特定位置明确说明。我可以让my_class<T>构造函数测试是否my_func<T>已被调用,如果没有,则生成运行时错误,但我更希望能够生成编译时错误,因为在编译时可以知道是否my_func<T>曾经使用过。

编辑[1]:一种不太理想的方法是制作一个MY_FUNC宏来创建一个不完整的模板类的模板特化......

template<typename T>
class incomplete;

#define MY_FUNC(T) template<> incomplete<T> { static const bool x = my_func<T>(); };

template<typename T>
class my_class {
    template<size_t i>
    class empty {};

    typedef empty<sizeof(incomplete<T>)> break_everything;
}

现在用户只能在某处使用my_class<T>if 。MY_FUNC(T)但是,我更喜欢不使用宏并且不强制用户MY_FUNC在全局范围内使用而不是在函数中使用的解决方案。

编辑[2]:感谢大家的回答。我意识到在编译时不可能保证用户不会my_func<T>错误地调用。但是我可以让它变得不太可能,并让它在早期产生运行时错误,如果他们这样做的话。

他们应该定义一个函数,比如说,initialise_library. 该函数如下所示:

void initialise_library() {
    my_func<int>();
    my_func<char>();
    etc..
}

在库初始化期间,它会检查以确保没有my_func调用任何 s。然后它调用initialise_library. 然后它会检查以确保所有的my_funcs都已被调用。这是可行的。如果用户没有定义initialise_library他们会得到一个链接器错误。如果他们没有my_func<T>在他们的程序中调用my_class<T>他们使用的某个地方,那么(理想情况下)他们会得到一个编译错误。如果他们已经定义initialise_library 并且他们已经在my_func<T>某个地方使用过,但是他们在错误的地方使用了它,那么当他们的程序启动时就会出现运行时错误。

基本上我想阻止他们my_class<T>在代码中的某个地方添加 a 并忘记my_func<T>()输入initialise_library,因为这是他们可能会做的事情。运行时检查的唯一方法是在构建my_class<T>. my_class<T>如果他们只在深度且很少使用的代码块中使用,这将是一个令人讨厌的后期检查。

4

3 回答 3

1

有很多事情让这很难做到,几乎可以肯定是不可能的。

首先,您必须了解在编译时可以知道的内容与特定编译器在编译时实际知道的内容之间存在差异,标准要求编译器应该跟踪哪些信息,以及您可以了解哪些信息实际提取和使用在编译时(以及在什么上下文中您可以使用该信息)。我认为您所问的在理论上是可能的(例如,在某些假设下,一切都发生在一个翻译单元中)。但我几乎可以肯定,该标准并不要求编译器能够为您提供此类信息,更不用说要求自己了解此类信息了。而且我也怀疑大多数编译器是否能够在除了最微不足道的情况之外的所有情况下都知道这些信息。

这样做的原因是简单的。您的问题实际上归结为询问编译器是否在 B 点之前的任何地方调用了函数 A。其中函数 A 是您的函数模板,而 B 点是实例化点和类模板的第一个创建点。这意味着当编译器到达 B 点时,它必须已经分析了与 B 点之前的可能执行点相对应的所有代码(并实例化了任何模板)。换句话说,它要求编译器必须遵循执行路径编译时贯穿始终。编译器不这样做。函数模板很容易被埋在其他一些函数中,编译器在到达 B 点之前不会编译,即使该函数将在创建类模板对象之前执行。反过来也可能是真的,我。

然后是分支的问题。实例化一个函数模板并不意味着它会被执行,因为某些条件语句或异常,甚至是这个问题的首选。编译器所做的分析必须非常疯狂才能确定地告诉您在您到达类模板的构造函数时该函数是否已确定执行。

然后是翻译单元的问题。如果函数模板实例化和类模板实例化没有出现在同一个翻译单元中怎么办?即使您有一个工作机制来检测函数模板已实例化,即使执行顺序正确,它也不会在这种情况下工作。

长话短说,使用运行时检查,或找到从构造函数调用函数的方法(可能使用默认参数和运行时的调试警告消息)。

编辑:通常,在编译时确保函数调用/构造函数调用的特定顺序的方法是将它们绑定到初始化链。一个简单的技巧是使类模板只能通过调用另一个类的成员函数(例如,在该类的全局/单例对象上)来构造,并使该对象只能通过调用具有所需参数的函数模板来构造。有许多类似的技巧,通常涉及静态局部变量作为“初始化一次”变量。但是,当然,这些使代码更加奇怪。

于 2013-05-09T06:15:43.947 回答
1

即使可以在编译时检测到对您的函数的调用,也完全不可能验证该调用实际上是在运行时进行的,并且它发生在其他调用之前。所以无论如何你必须使用运行时检查。

于 2013-05-09T05:58:35.597 回答
0

根据您的编辑,您可以尝试以下操作:

#include <iostream>
#include <stdexcept>
using namespace std;

template <typename T>
class my_class
{
public:
  static bool s_isInitialized;

  my_class()
  {
    if(!s_isInitialized) {
      throw logic_error("must call my_function<T> before instantiating my_class<T>");
    }
    cout << "ctor ok" << endl;
  }
};

template <typename T>
bool my_class<T>::s_isInitialized = false;

template <typename T>
void my_function(const char * type)
{
  cout << "initializing " << type << endl;
  my_class<T>::s_isInitialized = true;
}

int main()
{
  my_function<bool>("bool");
  my_class<bool> a;

  my_function<int>("int");
  my_class<int> b;
  my_class<int> c;

  cout << "now for something different" << endl;
  my_class<short> d;        // ctor throws
  // this code isn't reached
}

基本上,每个模板实例都有一个标志,该标志my_class<T>确定是否my_function<T>已调用适当的。将标志初始化为false,并将其设置为truein my_function<T>。然后的构造函数my_class<T>必须检查标志,如果它仍然为假,则抛出异常。

尽管检查是在运行时而不是像您想要的那样在编译时进行,但开销是疏忽的。它是对布尔值的单次检查,它是加载字节的一条指令和分支的一条指令。您不会为分支支付任何开销,因为任何半体面的硬件分支预测器将始终正确预测您的类的每个实例化(可能除了第一个或两个实例化)。

此外,您还可以添加一个签入my_function<T>以检查该标志是否尚未设置,logic_error如果初始化已经发生,则不执行任何操作或抛出另一个(如您认为合适的)。

如果您使用任何线程/并发,您当然必须防止并发访问标志/初始化函数。这将需要更多的编程开销,但这并不难。

于 2013-05-09T05:35:27.603 回答