【问题标题】:C++: Generate a compile error if user tries to instantiate a class template wihout instantiating a corresponding function templateC++:如果用户尝试实例化类模板而不实例化相应的函数模板,则会生成编译错误
【发布时间】:2013-05-09 15:11:27
【问题描述】:

在我正在编写的库中,我有一个类:

template<typename T>
class my_class {};

还有一个功能:

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

如果我的代码的用户尝试实例化my_class&lt;T&gt;,我想保证他们在他们的代码中的某处调用了my_function&lt;T&gt;。换句话说,如果他们尝试实例化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&lt;T&gt; 不需要在内部使用 my_func&lt;T&gt;,因此实例化它不会自动导致 my_func&lt;T&gt; 被实例化。

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

编辑[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_FUNC(T),则用户只能使用my_class&lt;T&gt;。但是,我更喜欢不使用宏且不强制用户在全局范围而不是在函数中使用 MY_FUNC 的解决方案。

编辑[2]:感谢大家的回答。我意识到在编译时不可能保证用户不会错误地调用my_func&lt;T&gt;。但我可以让它不太可能发生,并让它在早期产生运行时错误。

他们应该定义一个名为initialise_library 的函数。函数如下所示:

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

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

基本上,我想阻止他们在代码中的某处添加my_class&lt;T&gt; 并忘记将my_func&lt;T&gt;() 放入initialise_library,因为这是他们可能会做的事情。运行时检查的唯一方法是在my_class&lt;T&gt; 的构造过程中。如果他们只在深入且很少使用的代码块中使用my_class&lt;T&gt;,那么进行此检查将是一个令人讨厌的延迟时间。

【问题讨论】:

  • erm,如果您需要用户传递东西,那么您似乎在编译时没有足够的信息,因此检查构造函数似乎是唯一可行的选择
  • “如果我的代码的用户尝试实例化 my_class”,我想保证我的代码的用户在他们的代码中的某处调用了 my_function - 要么A) 糟糕的设计,或 B) 您需要记录的内容。尝试重新考虑整个决定。
  • int x = 0; cin &gt;&gt; x; if(x == 1) { my_class&lt;whatever&gt; m; } my_function&lt;whatever&gt;();你能告诉我是否在调用你的函数之前创建了一个实例吗?
  • 查看编辑[2]了解我如何进行此项检查。
  • 我在尝试将过程 C 库包装在 C++ 类(例如 OpenGL)中时遇到了类似的情况。因此,我理解为什么需要这个,但如果这真的是一个库,你永远不能指望调用者将所有初始化放在 1 个方法中并在此之后实例化类。您始终可以将此记录为使用该库的良好实践,但最终如何使用它取决于用户。所以尽量减少这些限制,这使得使用库非常困难。解决这个问题的一种方法可能是工厂模式。

标签: c++ templates


【解决方案1】:

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

【讨论】:

  • 这是真的,但我想尽可能避免错误地使用该库,并且有一些方法可以让他们难以在错误的地方调用 my_func。跨度>
  • @Shum:您基本上保证了您的库很难正确使用。我无法想象会写出这样的东西。
  • 不,它很容易正确使用。把函数调用放在文档说它应该去的地方,它就会工作。
  • 如果我在两个不同的翻译单元中实例化类模板怎么办?初始化函数需要调用两次吗?
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

    【解决方案3】:

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

    #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&lt;T&gt; 的每个模板实例都有一个标志,用于确定是否调用了适当的my_function&lt;T&gt;。将标志初始化为false,在my_function&lt;T&gt;中设置为truemy_class&lt;T&gt; 的构造函数必须检查标志,如果仍然为假则抛出异常。

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

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

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

    【讨论】:

    • 谢谢,但很遗憾我不能使用它。请参阅编辑[0]。
    • @Shum 好的,用新的解决方案编辑。仍然是运行时检查,但我认为您想要的编译时检查真的不可能。 (至少不是没有一些非常丑陋的预处理器黑客自己的垮台)
    猜你喜欢
    • 1970-01-01
    • 2014-04-20
    • 2019-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-18
    • 1970-01-01
    相关资源
    最近更新 更多