为什么我不能在另一个函数内定义一个函数?

83

这不是关于lambda函数的问题,我知道可以将lambda分配给变量。

允许我们在代码中声明但不定义函数的目的是什么呢?

例如:

#include <iostream>

int main()
{
    // This is illegal
    // int one(int bar) { return 13 + bar; }

    // This is legal, but why would I want this?
    int two(int bar);

    // This gets the job done but man it's complicated
    class three{
        int m_iBar;
    public:
        three(int bar):m_iBar(13 + bar){}
        operator int(){return m_iBar;}
    }; 

    std::cout << three(42) << '\n';
    return 0;
}

那么我想知道的是,为什么C++允许看起来无用的two和看起来更加复杂的three,但不允许one

编辑:

从答案中可以看出,在代码声明中可能能够防止命名空间污染,但是我希望听到的是为什么允许声明函数的能力,而禁止定义函数的能力。


3
第一个 one 是一个函数定义,另外两个是声明 - Some programmer dude
9
我觉得你把术语用反了--你想问的是“允许我们在代码内声明但不定义一个函数有什么意义?”而且,既然我们已经在这个话题上了,你可能想说“在函数内部”,因为所有的都是“代码”。 - Peter - Reinstate Monica
14
如果你想知道为什么这门语言有些怪癖和不一致:因为它经历了数十年的演变,通过许多人用许多不同的想法,从为不同目的而发明的语言中演化而来。如果你问为什么它有这个特定的怪癖:因为到目前为止没有人认为本地函数定义足够有用以标准化。 - Mike Seymour
4
@MikeSeymour说得很对。相比于Pascal等语言,C的结构不是很严谨,并且仅允许顶层函数定义。这是历史原因以及缺乏改变的需要。函数声明之所以可能,只是受一般情况下具有作用域的声明的影响。禁止函数声明将意味着额外的规则。 - Peter - Reinstate Monica
3
可能是因为通常情况下,允许在代码块中声明变量,没有特别禁止函数声明的原因;只允许所有声明似乎更加简单。但是,“为什么”并不是一个可以回答的问题;编程语言之所以存在就是因为它是这样演化而来的。 - Mike Seymour
显示剩余24条评论
11个回答

42
很难确定为什么不允许使用one,早在N0295中就提出了嵌套函数的建议:

我们讨论了将嵌套函数引入C ++的问题。 嵌套函数是众所周知的,它们的引入对编译器供应商、程序员或委员会都需要很少的努力。嵌套函数提供了显着的优点,[...]

显然这个提议被拒绝了,但由于我们没有1993年的会议记录可在网上获得,因此我们无法获得此拒绝的理由。

实际上,Lambda expressions and closures for C++也将此提议作为一个可能的替代方案:

一篇文章[Bre88]和提交给C ++委员会的N0295提案 [SH93]建议向C ++添加嵌套函数。嵌套函数类似于lambda表达式,但是定义为函数体内的语句,并且除非该函数处于活动状态,否则无法使用生成的闭包。这些提议还没有为每个lambda表达式添加新类型,而是像普通函数一样实现它们,包括允许特殊类型的函数指针引用它们。这些提议都早于向C ++添加模板,因此不会提到在与通用算法结合使用嵌套函数的情况下。此外,这些提议没有办法将局部变量复制到闭包中,因此它们生成的嵌套函数在其封闭函数之外完全无法使用。

考虑到我们现在已经有了lambda,因此不太可能看到嵌套函数,因为正如该论文所述,它们是同一问题的替代方案,相对于lambda,嵌套函数有几个限制。

至于您问题的这一部分:

// This is legal, but why would I want this?
int two(int bar);

有时这是调用所需函数的有用方式。草案C++标准第3.4.1[basic.lookup.unqual]给出了一个有趣的示例:

namespace NS {
    class T { };
    void f(T);
    void g(T, int);
}

NS::T parm;
void g(NS::T, float);

int main() {
    f(parm); // OK: calls NS::f
    extern void g(NS::T, float);
    g(parm, 1); // OK: calls g(NS::T, float)
}

1
关于您提供的3.4.1示例的问题:主函数中的调用者是否可以简单地编写::g(parm, 1)以便在全局命名空间中调用该函数?或者调用g(parm, 1.0f);,这应该会得到更好的匹配结果。 - Peter - Reinstate Monica
1
我想在这里添加评论:这个答案被接受并不是因为它最好地解释了为什么函数声明在代码中是允许的;而是因为它最好地描述了为什么在代码中不允许函数定义,这才是实际的问题。特别是它具体概述了为什么在代码函数的假设实现将与lambda的实现不同。+1 - Jonathan Mee
1
@JonathanMee:这句话:“...我们没有可能的来源来解释这个拒绝的理由。”怎么能够成为最好的描述为什么不允许嵌套函数定义(或者根本没有尝试去描述它)? - Jerry Coffin
@JerryCoffin 这个答案包括了为什么lambda已经是代码函数定义的超集,使它们的实现变得不必要的官方原理:“除非该函数处于活动状态,否则无法使用生成的闭包...此外,这些提议没有办法将局部变量复制到闭包中。” 我认为你问的是为什么我没有接受你对编译器增加额外复杂性分析的答案。如果是这样的话:你谈到了一些lambda已经完成的困难之处,代码定义显然可以像lambda一样被实现。 - Jonathan Mee
@JerryCoffin 让我总结一下我对这个答案的理解:“在代码中,函数定义在C++11功能集的开发过程中被重新审视,但它们被拒绝作为lambda功能的子集。” 我对你的帖子的理解:“在代码中,函数定义由于实现复杂性而被历史性地拒绝。目前,在代码中函数定义可在gcc中使用,或者在标准化的lambda中使用。” 当我总结这些答案时,它们似乎很相似,但是这个答案中明确的标准委员会的引用是让我信服的。 - Jonathan Mee
显示剩余4条评论

31

那么答案是“历史原因”。在C语言中,您可以在块级作用域内有函数声明,而C++设计人员没有看到移除该选项的好处。

一个示例用法是:

#include <iostream>

int main()
{
    int func();
    func();
}

int func()
{
    std::cout << "Hello\n";
}

我认为这是个不好的想法,因为提供一个声明与函数的真实定义不匹配很容易出现错误,导致未定义的行为,编译器也无法诊断。


10
“这通常被认为是一个糟糕的主意”- 需要引用出处。 - Richard Hodges
4
函数声明应该放在头文件中,函数的实现应该放在 .c 或 .cpp 文件中。因此,在函数定义内部包含这些声明会违反这两个准则之一。 - MSalters
2
它如何防止声明与定义不同? - Richard Hodges
6
我觉得你的意思是,将函数声明放在头文件中是一个普遍有用的常见做法。我认为没有人会反对这一点。但是我不明白的是为什么宣告外部函数时要放在函数作用域内被“普遍认为是个坏主意”。 - Richard Hodges
1
这个答案如果没有编辑评论会更好。在我看来,禁止它比当前情况要糟糕得多,因为禁止它意味着很多不再编译的遗留代码。此外,禁止这种做法并不能解决问题。解决问题的方法是禁止源文件中具有外部链接的函数声明。这是编码标准的工作,而不是语言规范的工作。 - David Hammen
显示剩余13条评论

23
在你给出的示例中,void two(int)被声明为一个外部函数,该声明仅在main函数的作用域内有效。如果您只想使名称twomain()中可用,以避免在当前编译单元中污染全局命名空间,那么这是合理的。
对评论的响应示例:
main.cpp:
int main() {
  int foo();
  return foo();
}

foo.cpp:

int foo() {
  return 0;
}

无需使用头文件。编译并链接。

c++ main.cpp foo.cpp 

程序将会编译并运行,同时按照预期返回0。


“two”也必须在文件中定义,这样不是还会导致污染吗? - Jonathan Mee
1
@JonathanMee 不,two() 可以在完全不同的编译单元中定义。 - Richard Hodges
我需要帮助理解它是如何工作的。你不必包含它声明的头文件吗?在这一点上,它会被声明,对吧?我只是不明白如何在代码中定义它,而又不包括声明它的文件? - Jonathan Mee
5
头文件并没有什么特别的地方,它们只是一个方便的地方来放置声明。函数内部的声明和头文件中的声明是一样有效的。所以,不,你不需要包含你链接的内容的头文件(甚至可能根本没有头文件)。 - Cubic
@Cubic,你的意思是说我可以在X.h文件中定义int foo(int),然后在X.cpp文件中实现int foo(int)?我想这很有道理。 - Jonathan Mee
1
@JonathanMee 在 C/C++ 术语中,定义和实现是同一回事。你可以随时声明一个函数,但你只能定义它一次。声明不需要在以 .h 结尾的文件中 - 你可以有一个文件 use.cpp,其中有一个调用 foo 的函数 bar(在其主体中声明 foo),以及一个文件 provides.cpp,定义了 foo,只要你不搞乱链接步骤,就可以很好地工作。 - Cubic

19

实际上,你可以做到这些事情,因为它们并不是很难做到。

从编译器的角度来看,在一个函数内部有一个函数声明是相当容易实现的。编译器需要一种机制来处理函数内部的其他声明(例如 int x;)。对于编写编译器的人来说,无论是否在解析另一个函数内的代码时调用该机制,都没有什么差别——它只是一个声明,所以当它看到足够的内容知道那里有一个声明时,就会调用处理声明的部分。

实际上,禁止在函数内部进行这些特定声明可能会增加额外的复杂性,因为编译器随后需要进行一个完全没有必要的检查,以查看是否已经正在查看函数定义内部的代码,并根据此决定是否允许或禁止此特定声明。

这就留下了嵌套函数有何不同的问题。嵌套函数之所以不同,是因为它会影响代码生成方式。在允许嵌套函数的语言中(例如 Pascal),通常预期嵌套函数中的代码可以直接访问其所嵌套的函数的变量。例如:

int foo() { 
    int x;

    int bar() { 
        x = 1; // Should assign to the `x` defined in `foo`.
    }
}

没有本地函数,访问本地变量的代码相当简单。在一个典型的实现中,当执行进入函数时,在堆栈上分配了一些局部变量的空间块。所有的局部变量都分配在这个单独的块中,每个变量被视为从块的开头(或结尾)的偏移量。例如,让我们考虑一个类似于以下代码的函数:

int f() { 
   int x;
   int y;
   x = 1;
   y = x;
   return y;
}

编译器(设想它没有优化掉额外的代码)可能会生成大约等效于以下代码的内容:

stack_pointer -= 2 * sizeof(int);      // allocate space for local variables
x_offset = 0;
y_offset = sizeof(int);

stack_pointer[x_offset] = 1;                           // x = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];     // y = x;
return_location = stack_pointer[y_offset];             // return y;
stack_pointer += 2 * sizeof(int);
特别地,它有一个指向局部变量块开始的位置,并且所有对局部变量的访问都是以该位置为偏移量的方式进行。
当涉及到嵌套函数时,情况就不再是这样了 - 相反,一个函数不仅可以访问自己的本地变量,还可以访问嵌套在其中的所有函数的局部变量。它不再只有一个“stack_pointer”用于计算偏移量,而是需要沿着堆栈向上查找以找到嵌套函数中的局部stack_pointers。
在一个微不足道的情况下,这并没有太大的问题 - 如果 bar 嵌套在 foo 中,则 bar 可以在堆栈中查找前一个 stack pointer 来访问 foo 的变量。对吗?
错误!嗯,有些情况下这可能是正确的,但不一定是这样。特别是如果 bar 是递归的话,那么给定的调用 bar 可能必须向上查找一些几乎任意数量的堆栈级别才能找到周围函数的变量。一般来说,你需要做以下两件事之一:要么在堆栈上放一些额外的数据,这样它就可以在运行时搜索回去找到其周围函数的堆栈帧,要么将指向周围函数的堆栈帧的指针作为嵌套函数的隐藏参数传递。哦,但也不一定只有一个周围函数 - 如果你可以嵌套函数,你可能可以任意地嵌套它们(或多或少),所以你需要准备好传递任意数量的隐藏参数。这意味着你通常会得到类似于周围函数的堆栈帧的链接列表,并且通过遍历该链接列表找到它的堆栈指针,然后从该堆栈指针访问偏移量来访问周围函数的变量。
然而,这意味着访问“本地”变量可能并不是一个微不足道的问题。找到正确的堆栈帧以访问变量可能是复杂的,因此访问周围函数的变量也(至少通常)比访问真正的局部变量慢。当然,编译器必须生成代码来找到正确的堆栈帧、通过任意数量的堆栈帧访问变量等等。
这就是 C 通过禁止嵌套函数而避免的复杂性。现在,毫无疑问,当前的 C++ 编译器与 1970 年代的 C 编译器完全不同。对于像多重虚拟继承之类的东西,C++ 编译器无论如何都必须处理这些基本的问题(例如,在这种情况下找到基类变量的位置也可能是复杂的)。在百分比上,支持嵌套函数不会给当前的 C++ 编译器增加太多的复杂性(一些编译器,如 gcc,已经支持它们)。
同时,它很少添加太多实用程序。特别地,如果你想定义一个在函数内部“表现”得像函数的东西,你可以使用 lambda 表达式。实际上创建的是一个对象(即某个类的实例),该对象重载了函数调用运算符 (operator()),

5
第一个是函数定义,这是不允许的。显然,在另一个函数内部定义函数有什么用处呢?
但是第二和第三个只是声明。想象一下,在主方法中需要使用int two(int bar);函数。但它在main()函数下面被定义了,所以函数声明可以让你在函数内部使用该函数。
第三个也是同样的道理。在函数内部进行类声明可以让你在函数内部使用类而不需要提供适当的头文件或引用。
int main()
{
    // This is legal, but why would I want this?
    int two(int bar);

    //Call two
    int x = two(7);

    class three {
        int m_iBar;
        public:
            three(int bar):m_iBar(13 + bar) {}
            operator int() {return m_iBar;}
    };

    //Use class
    three *threeObj = new three();

    return 0;
}

2
什么是“减速”?你是指“声明”吗? - Peter Mortensen

4
实际上,有一个用例可能会很有用。如果您想确保某个函数被调用(并且您的代码可以编译),无论周围的代码声明什么,您都可以打开自己的块并在其中声明函数原型。(灵感最初来自Johannes Schaub,https://dev59.com/1EfSa4cB1Zd3GeqPAMtY#929902,通过TeKa,https://dev59.com/f3NA5IYBdhLWcg3wQ7Yw#8821992)。
如果您必须包含您无法控制的头文件或者如果您有一个可能在未知代码中使用的多行宏,则这可能特别有用。
关键是本地声明优先于最近封闭块中的先前声明。虽然这可能会引入微妙的错误(我认为,在C#中是禁止的),但可以有意识地使用。考虑:
// somebody's header
void f();

// your code
{   int i;
    int f(); // your different f()!
    i = f();
    // ...
}

链接可能很有趣,因为这些头文件很可能属于一个库,但我猜你可以调整链接器参数,使得在考虑该库时f()被解析为你的函数。或者你告诉它忽略重复的符号。或者你不链接该库。

帮我看看,你的例子中 f 会在哪里定义?这两个函数只有返回类型不同,难道不会导致函数重定义错误吗? - Jonathan Mee
@JonathanMee 嗯... f() 可能在不同的翻译单元中定义,我想。但是如果您还链接到假定的库,那么链接器可能会反对,我认为您是正确的。所以您不能这样做 ;-),或者至少必须忽略多个定义。 - Peter - Reinstate Monica
不好的例子。在C++中,void f()int f()之间没有区别,因为函数的返回值不是函数签名的一部分。将第二个声明更改为int f(int),我会取消我的踩票。 - David Hammen
@DavidHammen 在声明 void f() 之后尝试编译 i = f();。 “无区别”只是半真话;-)。我实际上使用了不可重载的函数“签名”,因为否则在C++中整个情况就是不必要的,因为两个具有不同参数类型/数量的函数可以和平共处。 - Peter - Reinstate Monica
@DavidHammen 在阅读了Shafik的回答后,我认为我们有三种情况:1.参数不同的签名。在C++中没有问题,简单的重载和最佳匹配规则可以解决。2.签名完全相同。在语言层面上没有问题;函数通过链接到所需的实现来解析。3.差异仅在返回类型上。在语言层面上确实存在问题,如所示;重载分辨率无法工作;我们必须声明一个具有不同签名的函数,并适当地进行链接。 - Peter - Reinstate Monica

4
这种语言特性是从C语言继承而来的,在C早期可能有一些作用(也许是函数声明作用域?)。我不知道现代C程序员是否经常使用这个特性,但我真的很怀疑。
因此,总结答案:
在现代C ++中没有这个特性的目的(至少我不知道),这是因为C ++向后兼容C(我想 :))。
感谢下面的评论:
函数原型的作用域限定在它声明的函数中,因此可以通过引用外部函数/符号而无需包含头文件,使全局命名空间更加整洁。

控制名称的范围以避免全局命名空间污染。 - Richard Hodges
好的,我认为它在想要引用外部函数/符号而不使用 #include 污染全局命名空间的情况下很有用!感谢您指出这一点。我会进行编辑。 - pdeschain

3

我并不是回答问题,而是对几条评论进行回复。

我不同意以下几点的评论和答案:1 嵌套声明被称为无害,2 嵌套定义是没有用的。

1 嵌套函数声明所谓无害的主要反例是臭名昭著的最恼人解析。在我看来,由此引起的混乱足以引入一项额外规则,禁止嵌套声明。

2 嵌套函数定义所谓无用的第一个反例是需要在完全相同的一个函数内的多个位置执行相同的操作。这方面有一个明显的解决方法:

private:
inline void bar(int abc)
{
    // Do the repeating operation
}

public: 
void foo()
{
    int a, b, c;
    bar(a);
    bar(b);
    bar(c);
}

然而,这种解决方案往往会在类定义中添加许多私有函数,每个函数仅在一个调用程序中使用。嵌套函数声明会更加简洁。

1
我认为这很好地概括了我提问的动机。如果您查看我引用的原始版本,我提到了MVP,但是在评论中(我的问题的评论中)被告知MVP是不相关的,我一直被否决。我只是想不明白为什么可能有害的代码声明仍然存在,而可能有用的代码定义却不存在。我为您提供的有益示例点赞。 - Jonathan Mee

2

具体回答这个问题:

从答案中看,似乎在代码中声明可能能够防止命名空间污染,但我希望听到的是为什么允许声明函数的能力,而禁止定义函数的能力。

因为考虑以下代码:

int main()
{
  int foo() {

    // Do something
    return 0;
  }
  return 0;
}

语言设计者的问题:

  1. foo()函数是否应该对其他函数可用?
  2. 如果是,它的名称应该是什么?int main(void)::foo()
  3. (请注意,在C++的起源C中不可能出现2)
  4. 如果我们想要一个局部函数,我们已经有了一种方法-将其作为本地定义类的静态成员。那么我们应该添加另一种实现相同结果的语法方法吗?为什么要这样做?这不会增加C++编译器开发人员的维护负担吗?
  5. 等等...

这个行为显然是针对lambda函数定义的,为什么不是针对代码中定义的函数呢? - Jonathan Mee
Lambda仅仅是编写函数对象的简写。捕获不带参数的lambda的特殊情况等同于本地函数定义,就像编写没有数据成员的函数对象一样。 - Richard Hodges
我只是指出,Lambda表达式和在代码中声明的函数已经驳斥了你所有的观点。这不应该增加任何“负担”。 - Jonathan Mee
@JonathanMee 如果你对此有强烈的感觉,可以向C++标准委员会提交RFC。 - Richard Hodges
Shafik Yaghmour的回答已经涵盖了这一点。如果他们不让我们定义函数,我个人希望看到在代码中声明函数的能力被删除。Richard Hodges的回答很好地解释了为什么我们仍然需要在代码声明中声明的能力。 - Jonathan Mee

1

我想指出,GCC编译器允许在函数内声明函数。在这里了解更多信息。此外,随着C++引入lambda表达式,这个问题现在有点过时了。


在其他函数内部声明函数头的能力,我发现在以下情况下非常有用:
void do_something(int&);

int main() {
    int my_number = 10 * 10 * 10;
    do_something(my_number);

    return 0;
}

void do_something(int& num) {
    void do_something_helper(int&); // declare helper here
    do_something_helper(num);

    // Do something else
}

void do_something_helper(int& num) {
    num += std::abs(num - 1337);
}

这里有什么?基本上,你有一个函数应该从主函数中调用,所以你像平常一样进行前向声明。但是你意识到,这个函数还需要另一个函数来帮助它完成它正在做的事情。因此,你不是在主函数上面声明那个辅助函数,而是在需要它的函数内部声明它,然后它可以从那个函数中调用,并且只能从那个函数中调用。
我的观点是,在函数内部声明函数头可以是函数封装的一种间接方法,它允许函数通过委托给某些其他函数来隐藏它正在做的某些部分,而只有它自己知道,几乎给人一种嵌套函数的错觉。

我理解我们可以内联定义lambda表达式。我知道我们可以内联声明函数,但这是最令人烦恼的解析的起源,所以我的问题是,如果标准将保留仅用于引起程序员愤怒的功能,那么程序员也应该能够内联定义函数吗?Richard Hodges的答案帮助我理解了这个问题的起源。 - Jonathan Mee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接