【问题标题】:Ensure at compile time that a method is called in exactly one place确保在编译时在一个地方调用一个方法
【发布时间】:2020-07-07 19:38:19
【问题描述】:

我很好奇是否有可能在编译时确保在一个地方调用一个方法。

请注意,如果函数被多次调用(例如在一个循环中)是可以的 - 但不应在两个单独的循环中调用它。

这可以分为两部分,我也对涵盖任一部分的解决方案感兴趣:
(a) 确保至少在一个地方调用方法
(b) 确保一个方法最多在一个地方被调用

我可以完全控制代码的结构,欢迎使用不同的成语来实现相同的想法。

// class.h

class MyClass {
  public:
    void my_method();
}

以下内容不应编译(从不调用)

#include "class.h"

int main() {
  MyClass my_class;
}

以下内容不应编译(在多个地方调用)

#include "class.h"

int main() {
  MyClass my_class;
  my_class.my_method();
  while(true) {
    my_class.my_method();
  }
}

以下内容应该编译(只在一个地方调用):

#include "class.h"

int main() {
  MyClass my_class;
  while(true) {
    my_class.my_method();
  }
}

【问题讨论】:

  • 不要让它成为一种方法。将代码内联放在那个地方。
  • 我认为您也可以使用 lambda(它可能是一个空的 lambda)来执行此操作,因为每个 lambda 的闭包类型都是唯一的。同样,这将是一个运行时错误,但这不是您所要求的。如果您提供有关您要解决的问题的更多详细信息,那么我们或许能够找到解决此问题的方法。
  • 您可以使用非标准的__COUNTER__ 宏来执行此操作。像static_assert(__COUNTER__ == 0); my_class.my_method(); 这样的东西。但是,计数器会在每个翻译单元中重置,因此您只能检查每个翻译单元是否调用了该函数。
  • 你为什么要这样做?函数的部分意义在于它可以从多个地方调用。
  • 你应该解释为什么你想这样做。也许您要求的解决方案不是实现您真正目标的最佳解决方案。

标签: c++


【解决方案1】:

低技术方法:

由于您可以控制代码结构(我假设其中包括构建系统),因此这是一个低技术解决方案:

  • 使函数名足够唯一
  • grep 获取代码中的函数名称。您期望它两次(假设您的声明和定义位于同一位置):
    • 在标题中一次
    • 曾在单一呼叫站点

或者:

如果你真的、真的、真的想用 C++ 解决它,那么你可以试试

  • 使用编译时间计数器计算编译单元内的使用次数
  • 确保如果标头包含在多个编译单元中,该函数将违反 ODR。

但是,编译时间计数器是黑魔法(我说,我真的很喜欢 TMP),并且为此目的强制违反 ODR 似乎是类似的巫术(至少您需要一个无法链接的测试用例)。

但说真的:

不要这样做。无论你做什么,它都可以通过包装函数几乎不费吹灰之力:

auto call_my_method(MyClass& o)
{
   return o.my_method();
}

MyClass::my_method() 仅在包装器中调用。其他人只是调用包装器,它甚至可能被编译器内联。

正如其他人所建议的那样:如果您能解释一下您要做什么,可能会更有帮助。

【讨论】:

    【解决方案2】:

    这是一个可能可行的粗略想法(评论太长 - 但对于一个好的 SO 答案来说不完整)。

    您可以通过计算/检查模板实例来实现此目的。
    模板仅在使用时被实例化。

    同样,如果模板方法/函数体从未被调用,则它们不会被解析、编译或链接(除了确保有效的语法之外)。这意味着不会在它们的主体内进行任何实例化)。

    您可以创建一个模板来维护一些全局实例化计数和静态断言(或一些其他 TMP 机制来检查过去的实例化)。

    【讨论】:

    • “全局”实例化计数对于当前编译单元来说是本地的。
    【解决方案3】:

    使用 C 预处理器和 GNU 内联汇编可以部分解决这个问题:

    头文件a.h:

    struct A {
        // Do not call this method directly, use the macro below to call it
        int _method_vUcaJB5NKSD3upQ(int i, int j);
    };
    
    // Use inline assembly to ensure that this macro is used at most once
    #define method_vUcaJB5NKSD3upQ(args...) \
        _method_vUcaJB5NKSD3upQ(args); \
        asm (".global once_vUcaJB5NKSD3upQ; once_vUcaJB5NKSD3upQ:");
    

    实现文件a.cc:

    #include <iostream>
    #include "a.h"
    
    int A::_method_vUcaJB5NKSD3upQ(int i, int j) { return i+j+5; }
    
    // Ensure that the macro is used at least once
    extern "C" const char once_vUcaJB5NKSD3upQ;
    static const char get_vUcaJB5NKSD3upQ = once_vUcaJB5NKSD3upQ;
    
    int main() {
        A a;
        for(int i=0; i<7; i++) {
            // Use a separate statement to call the method
            // (terminated by a semicolon, it cannot be a sub-expression)
            auto x = a.method_vUcaJB5NKSD3upQ(2, 3);
            std::cout << x << std::endl;
        }
        return 0;
    }
    

    这个解决方案是部分的,它不会阻止程序直接调用以下划线开头的方法而不使用包装宏。

    【讨论】:

      【解决方案4】:

      使用 constexpr 计数器。在另一个question中有一个实现@

      【讨论】:

      • 听起来这个方法格式不正确。
      • 在对该问题的回复中引用的问题open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2118 指出这是标准的一个错误,应该是格式错误的。
      • 所以它不是病态的,至少现在还不是?
      • 如果它还不是格式错误的,它应该被尽可能多的人尽快使用,这样他们就必须支持这个用例!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-01
      相关资源
      最近更新 更多