一个什么都不做的函数有什么实际用途吗?

一个在运行时什么都不做的函数有什么用处吗,例如:

void Nothing() {}

注意,我所说的函数并不像 sleep()那样需要等待一定的时间,而只是编译器/解释器给它的时间。

10384 次浏览

是的。有相当多的事情需要一个函数来通知发生的特定事情(回调)。一个不执行任何操作的函数是表示“我不关心这个”的好方法

我不知道标准库中有什么示例,但是许多构建在标准库之上的库都有针对事件的函数指针。

例如,GLib 定义了一个回调“ GLib。LogFunc (log _ domain,log _ level,message,* user _ data)”,用于提供日志记录器。空函数是禁用日志记录时提供的回调。

一个接受参数但不对其执行任何操作的函数可以与一个执行有用操作的函数成对使用,这样即使使用了 no-op 函数,参数仍然可以被计算。这在日志记录场景中非常有用,在这种场景中,必须对参数进行计算,以验证表达式是否合法,并确保发生任何重要的副作用,但是日志记录本身是不必要的。当编译时日志记录级别设置为不希望输出特定日志语句的级别时,预处理器可以选择 no-op 函数。

这样的函数作为回调函数是必需的。

假设你有一个这样的函数:

void do_something(int param1, char *param2, void (*callback)(void))
{
// do something with param1 and param2
callback();
}

此函数接收一个指向函数的指针,然后调用该函数。如果您不特别需要对任何事情使用这个回调函数,那么您将传递一个不执行任何操作的函数:

do_something(3, "test", Nothing);

当我创建包含函数指针的表时,我会使用空函数。

例如:

typedef int(*EventHandler_Proc_t)(int a, int b); // A function-pointer to be called to handle an event
struct
{
Event_t             event_id;
EventHandler_Proc_t proc;
}  EventTable[] = {    // An array of Events, and Functions to be called when the event occurs
{  EventInitialize, InitializeFunction },
{  EventIncrement,  IncrementFunction  },
{  EventNOP,        NothingFunction    },  // Empty function is used here.
};

在这个示例表中,我将 NULL放在 NothingFunction的位置,并在调用它之前检查 .proc是否为 NULL。但是我认为在表中放置一个不执行任何操作的函数可以使代码更简单。

一个用例可能是程序开发过程中的一个临时 票根函数。

如果我正在进行一些自顶向下的开发,我通常会设计一些函数原型,编写 main 函数,然后运行编译器,看看到目前为止是否有语法错误。为了实现编译,我需要实现相关的函数,我将首先创建空的“存根”,它们什么也不做。一旦我通过了编译测试,我就可以继续并且一次充实一个函数。

我用来教书的加迪斯教科书 从 C + + 开始: 从控制结构到对象是这样描述它们的(第6.16节) :

存根是一个虚函数,它被调用而不是实际的 它通常显示一个测试消息 承认它的存在,仅此而已。

据我回忆,在 Lions 对 UNIX 第6版的评论,附源代码中有两个空函数,本世纪初重新发行的介绍称为 Ritchie,Kernighan 和 Thompson。

实际上,在 C 语言中,吞噬参数并且不返回任何值的函数是普遍存在的,但是并没有显式地写出来,因为几乎每一行都隐式地调用了它。在传统 C 语言中,这个空函数最常见的用法是对任何语句的值进行不可见的丢弃。但是,由于 C89,这可以明确拼写为 (void)。每当函数返回值被忽略而没有显式地传递给这个不返回任何值的内置函数时,lint工具就会发出抱怨。这背后的动机是为了防止程序员无声无息地忽略错误条件,您仍然会遇到一些使用编码风格 (void)printf("hello, world!\n");的旧程序。

这种功能可用于:

  • 回调(其他回答已经提到了)
  • 高阶函数的一个参数
  • 对框架进行基准测试,无需执行无操作的开销
  • 具有可以比较其他函数指针的正确类型的唯一值。(特别是在像 C 这样的语言中,所有的函数指针都是可转换的,并且可以彼此比较,但是函数指针和其他类型的指针之间的转换是不可移植的。)
  • 单值类型的唯一元素,在函数式语言中
  • 如果传递了它严格计算的参数,这可能是放弃返回值但执行副作用和测试异常的一种方法
  • 一个假的占位符
  • 在有类型λ演算中证明某些定理

从语言律师的角度来看,不透明的函数调用为优化插入了一个障碍。

对于 例子:

int a = 0;


extern void e(void);


int b(void)
{
++a;
++a;
return a;
}


int c(void)
{
++a;
e();
++a;
return a;
}


int d(void)
{
++a;
asm(" ");
++a;
return a;
}

b函数中的 ++a表达式可以合并到 a += 2,而在 c函数中,a需要在函数调用之前更新,之后从内存中重新加载,因为编译器无法证明 e不访问 a,类似于(非标准) d函数中的 asm(" ")

无为函数的另一个临时用途可以是存在一行来放置断点,例如,当您需要检查传递到新创建的函数中的运行时值时,这样您就可以更好地决定将要放入的代码需要访问哪些内容。就个人而言,我喜欢使用自我分配,也就是说,当我需要这种断点时,使用 i = i,但是一个 no-op 函数可能也同样适用。

void MyBrandNewSpiffyFunction(TypeImNotFamiliarWith whoKnowsWhatThisVariableHas)
{
DoNothing(); // Yay! Now I can put in a breakpoint so I can see what data I'm receiving!
int i = 0;
i = i;    // Another way to do nothing so I can set a breakpoint
}

在嵌入式固件世界中,它可以用来添加一个微小的延迟,这是由于某些硬件原因所需要的。当然,这也可以一次调用多次,这样程序员就可以扩展这种延迟。

空函数在特定于平台的抽象层中并不少见。通常有些函数只在某些平台上需要。例如,函数 void native_to_big_endian(struct data* d)在小端 CPU 上包含字节交换代码,但在大端 CPU 上可能是完全空的。这有助于保持业务逻辑平台的不可知性和可读性。我也见过这样的事情,比如将本机文件路径转换为 Unix/Windows 风格,硬件初始化功能(当一些平台可以运行默认值,其他平台必须主动重新配置)等等。

冒着被认为跑题的风险,我将从托马斯主义的角度来论证一个什么都不做的函数,以及计算中的 NULL 的概念,在计算中真的没有任何位置。

软件实质上是由属于行为的状态、行为和控制流组成的。没有国家是不可能的,没有行为是不可能的。

不存在状态是不可能的,因为值始终存在于内存中,而不管可用内存的初始化状态如何。没有行为是不可能的,因为不能执行非行为(甚至“ nop”指令也不能执行某些操作)。

相反,我们可以更好地指出,存在着由上下文主观定义的消极存在和积极存在,其客观定义是,状态或行为的消极存在分别意味着没有明确的价值或实现,而积极存在则分别指明确的价值或实现。

这改变了有关 API 设计的观点。

而不是:

void foo(void (*bar)()) {
if (bar) { bar(); }
}

取而代之的是:

void foo();


void foo_with_bar(void (*bar)()) {
if (!bar) { fatal(__func__, "bar is NULL; callback required\n"); }
bar();
}

或:

void foo(bool use_bar, void (*bar)());

或者你想知道更多关于存在的信息吧:

void foo(bool use_bar, bool bar_exists, void (*bar)());

其中的每一个都是更好的设计,使您的代码和意图得到很好的表达。一个简单的事实是,事物的存在与否关系到算法的运行,或者关系到解释状态的方式。如果将 NULL 保留为0(或任意值) ,不仅会丢失整个值,而且会使算法模型不那么完美,在极少数情况下甚至容易出错。更重要的是,在没有保留这个保留值的系统上,实现可能不会像预期的那样工作。

如果您需要检测输入是否存在,那么在您的 API 中应该明确地说明这一点: 如果输入非常重要,那么就为它设置一两个参数。由于要将逻辑元数据与输入分离,因此它将更具可维护性和可移植性。

因此,在我看来,一个什么都不做的函数并不实用,但是如果是 API 的一部分,那么它就是一个设计缺陷,如果是实现的一部分,那么它就是一个实现缺陷。NULL 显然不会那么容易消失,我们只是使用它,因为这是目前被必要性使用的,但在未来,它不一定是这样的。

除了这里已经给出的所有原因之外,请注意,“空”函数从来不会真正为空,因此通过查看汇编输出,您可以了解函数调用如何在所选的体系结构上工作。我们来看几个例子。假设我有以下 C 文件 nothing.c:

void DoNothing(void) {}

使用 clang -c -S nothing.c -o nothing.s在 x86 _ 64机器上编译这个文件,您将得到类似于下面这样的内容(删除元数据和与本文讨论无关的其他内容) :

没什么:

_Nothing:                               ## @Nothing
pushq   %rbp
movq    %rsp, %rbp
popq    %rbp
retq

看起来可不像没事。请注意将 %rbp(框架指针)推入和弹出到堆栈上。现在让我们更改编译器标志并添加 -fomit-frame-pointer,或者更明确地说: clang -c -S nothing.c -o nothing.s -fomit-frame-pointer

没什么:

_Nothing:                               ## @Nothing
retq

这看起来更像是“ Nothing”,但您仍然至少有一条 x86 _ 64指令正在执行,即 retq

让我们再试一次。 Clang 支持 gccgprof分析器选项 -pg,所以如果我们尝试这个选项会怎样: clang -c -S nothing.c -o nothing.s -pg

没什么:

_Nothing:                               ## @Nothing
pushq   %rbp
movq    %rsp, %rbp
callq   mcount
popq    %rbp
retq

在这里,我们添加了一个神秘的附加调用到一个函数 mcount(),编译器已经为我们插入。这个看起来一无所有。

所以你明白了。编译器选项和体系结构可以对函数中“无”的含义产生深远的影响。有了这些知识,您可以对如何编写代码以及如何编译代码做出更明智的决定。此外,这样一个调用数百万次并经过度量的函数可以非常精确地度量所谓的“函数调用开销”,或者根据您的架构和编译器选项进行调用所需的最短时间。实际上,在现代的超标量指令排程中,这种测量方法并不意味着什么,也不是特别有用,但在某些较老或“较简单”的架构中,它可能意义重大。

这些函数在测试驱动开发中占有重要的地位。

class Doer {
public:
int PerformComplexTask(int input) { return 0; } // just to make it compile
};

所有内容都会被编译,测试用例会说 Fail,直到函数被正确实现。