为啥我不能在另一个函数中定义一个函数?
Posted
技术标签:
【中文标题】为啥我不能在另一个函数中定义一个函数?【英文标题】:Why can't I define a function inside another function?为什么我不能在另一个函数中定义一个函数? 【发布时间】:2015-07-10 02:58:58 【问题描述】:这不是 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
?
编辑:
从答案看来,代码内声明可能能够防止命名空间污染,但我希望听到的是为什么允许声明函数的能力但不允许定义函数的能力。
【问题讨论】:
第一个,one
是一个函数定义,另外两个是声明。
我认为您用错了术语——您想问“允许我们在代码中声明但不定义函数有什么意义?”。当我们在这里时,您可能是指“在函数中”。都是“代码”。
如果您要问为什么该语言有怪癖和不一致之处:因为它经过数十年的演变,通过许多具有许多不同想法的人的工作,从在不同时间出于不同原因发明的语言。如果你问为什么它有这个特殊的怪癖:因为(到目前为止)没有人认为本地函数定义足够有用以进行标准化。
@MikeSeymour 说得对。 C 的结构不如 Pascal 好,并且始终只允许***函数定义。所以原因是历史性的,加上缺乏改变它的需要。该函数 declarations 是可能的只是通常可能的范围声明的结果。禁止函数将意味着一个额外的规则。
@JonathanMee:可能是因为一般来说,块中允许声明,并且没有特别的理由专门禁止函数声明;只允许没有特殊情况的任何声明更简单。但是“为什么”并不是一个真正可以回答的问题。语言之所以如此,是因为它就是这样进化的。
【参考方案1】:
为什么one
不被允许并不明显;嵌套函数是很久以前在N0295 中提出的,它说:
我们讨论在 C++ 中引入嵌套函数。嵌套 功能很好理解,它们的介绍几乎不需要 编译器供应商、程序员或委员会的努力。 嵌套函数具有显着优势,[...]
很明显,这个提议被拒绝了,但由于我们没有在线提供1993
的会议纪要,我们无法找到拒绝的理由。
事实上,这个提议在 Lambda expressions and closures for C ++ 中被注明为一种可能的替代方案:
一篇文章 [Bre88] 和提案 N0295 给 C ++ 委员会 [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)
【讨论】:
您给出的 3.4.1 示例中的问题: main 中的调用者不能简单地编写::g(parm, 1)
以调用全局命名空间中的函数吗?或致电g(parm, 1.0f);
,这应该会更好地匹配所需的g
?
@PeterSchneider 我那里声明太强了,我调整了。
我想在这里添加评论:这个答案被接受不是因为它很好地解释了为什么允许在代码中声明函数;但是因为它在描述为什么不允许在代码中定义函数时做得最好,这是实际的问题。具体来说,它特别概述了为什么代码中函数的假设实现与 lambda 的实现不同。 +1
@JonathanMee:到底是怎么做到的:“......我们没有可能的来源来解释这次拒绝的理由。”有资格作为描述为什么不允许嵌套函数定义的最佳工作(甚至试图描述它?)
@JerryCoffin 答案包括官方解释为什么 lambdas 已经是代码函数定义中的超级集合,使得它们的实现变得不必要:“除非该函数处于活动状态,否则无法使用生成的闭包......另外,这些提议无法将局部变量复制到闭包中。”我假设您在问为什么您对编译器的额外复杂性的分析不是我接受的答案。如果是这样:您谈到了 lambdas 已经完成的事情的难度,在代码中定义显然可以完全像 lambdas 一样实现。【参考方案2】:
嗯,答案是“历史原因”。在 C 中,您可以在块范围内进行函数声明,而 C++ 设计者并没有看到删除该选项的好处。
一个示例用法是:
#include <iostream>
int main()
int func();
func();
int func()
std::cout << "Hello\n";
IMO 这是一个坏主意,因为提供与函数的实际定义不匹配的声明很容易出错,从而导致编译器无法诊断的未定义行为。
【讨论】:
“这通常被认为是个坏主意” - 需要引用。 @RichardHodges:嗯,函数声明属于头文件,而实现在 .c 或 .cpp 文件中,因此在函数定义中包含这些声明违反了这两个准则中的任何一个。 如何防止声明与定义不同? @JonathanMee:我的意思是,如果您使用的声明在定义函数的地方不可用,编译器可能不会检查声明是否与定义匹配。因此,您可能有一个本地声明some_type f();
,以及另一个翻译单元中的定义 another_type f() ...
。编译器无法告诉您这些不匹配,并且使用错误的声明调用 f
将给出未定义的行为。因此,最好在标头中仅包含一个声明,并在该标头中包含定义函数的位置以及使用它的位置。
我认为您所说的是,将函数声明放在头文件中的常见做法通常很有用。我想没有人会不同意这一点。我认为没有理由断言在函数范围内声明外部函数“通常被认为是一个坏主意”。【参考方案3】:
在您给出的示例中,void two(int)
被声明为外部函数,该声明仅在 main
函数的范围内有效。
如果您只希望在 main()
中提供名称 two
以避免污染当前编译单元内的全局命名空间,这是合理的。
响应 cmets 的示例:
main.cpp:
int main()
int foo();
return foo();
foo.cpp:
int foo()
return 0;
不需要头文件。编译和链接
c++ main.cpp foo.cpp
它会编译运行,程序会按预期返回 0。
【讨论】:
不会two
也必须在文件中定义从而造成污染吗?
@JonathanMee no,two()
可以在完全不同的编译单元中定义。
我需要帮助来了解它是如何工作的。您不必包含声明它的标头吗?在什么时候它会被宣布,对吧?我只是不明白如何在代码中定义它,并且不包含声明它的文件?
@JonathanMee 标题没有什么特别之处。它们只是放置声明的方便地方。函数中的声明与标头中的声明一样有效。所以,不,你不需要包含你链接的标题(甚至可能根本没有标题)。
@JonathanMee 在 C/C++ 术语中,定义和实现是一回事。您可以根据需要多次声明一个函数,但您只能定义一次。声明不需要在以 .h 结尾的文件中 - 您可以有一个文件 use.cpp,它有一个调用 foo 的函数 bar(在其主体中声明 foo),以及一个定义 foo 的文件提供.cpp,只要您不搞乱链接步骤,它就可以正常工作。【参考方案4】:
你可以做这些事情,主要是因为它们实际上并不难做到。
从编译器的角度来看,在另一个函数中声明函数是很容易实现的。编译器需要一种机制来允许函数内部的声明处理函数内部的其他声明(例如,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_pointer。
现在,在一个并不那么糟糕的小例子中——如果bar
嵌套在foo
内,那么bar
可以在前一个堆栈指针处查找堆栈以访问foo
' s 变量。对吧?
错了!好吧,在某些情况下这可能是真的,但不一定是这样。特别是,bar
可能是递归的,在这种情况下,bar
的给定调用可能必须在堆栈中查找几乎任意数量的级别才能找到周围函数的变量。一般来说,您需要做以下两件事之一:要么在堆栈上放置一些额外的数据,以便它可以在运行时搜索堆栈以找到其周围函数的堆栈帧,要么您有效地将指针传递给周围函数的堆栈帧作为嵌套函数的隐藏参数。哦,但也不一定只有一个周围的函数——如果你可以嵌套函数,你可以将它们嵌套(或多或少)任意深度,所以你需要准备好传递任意数量的隐藏参数。这意味着您通常最终会得到类似于堆栈帧到周围函数的链表,并且访问周围函数的变量是通过遍历该链表以找到其堆栈指针,然后访问该堆栈指针的偏移量来完成的。
然而,这意味着访问“本地”变量可能不是一件小事。找到正确的堆栈帧来访问变量并非易事,因此访问周围函数的变量也(至少通常)比访问真正的局部变量要慢。而且,当然,编译器必须生成代码来找到正确的堆栈帧,通过任意数量的堆栈帧访问变量等等。
这是C通过禁止嵌套函数避免的复杂性。现在,可以肯定的是,当前的 C++ 编译器与 1970 年代的老式 C 编译器完全不同。对于多重虚拟继承之类的事情,C++ 编译器在任何情况下都必须以相同的一般性质处理事情(即,在这种情况下找到基类变量的位置也很重要)。按百分比计算,支持嵌套函数不会给当前的 C++ 编译器增加太多复杂性(有些编译器,例如 gcc,已经支持它们)。
同时,它也很少增加太多实用性。特别是,如果你想在函数内部定义作用的东西,你可以使用 lambda 表达式。这实际上创建的是一个重载函数调用运算符 (operator()
) 的对象(即某个类的实例),但它仍然提供类似函数的功能。不过,它使从周围上下文中捕获(或不捕获)数据更加明确,这允许它使用现有机制,而不是发明一个全新的机制和一组使用规则。
底线:尽管最初看起来嵌套声明很难并且嵌套函数微不足道,但或多或少恰恰相反:嵌套函数实际上比嵌套声明要复杂得多。
【讨论】:
【参考方案5】:第一个是函数定义,不允许。显然,wt 是将一个函数的定义放在另一个函数中的用法。
但其他两个只是声明。想象一下,您需要在 main 方法中使用 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;
【讨论】:
什么是“减速”?你的意思是“声明”吗?【参考方案6】:这个语言特性是从 C 继承而来的,它在 C 的早期有一些用途(可能是函数声明范围?)。 我不知道现代 C 程序员是否经常使用这个功能,我对此表示怀疑。
所以,总结一下答案:
现代 C++(至少我知道)中没有这个特性的目的,因为 C++ 到 C 的向后兼容性(我想 :))。
感谢下面的评论:
函数原型的作用域仅限于声明它的函数,因此可以拥有一个更整洁的全局命名空间 - 通过引用外部函数/符号而不使用 #include
。
【讨论】:
目的是控制名称的范围,避免全局命名空间污染。 好吧,我想它在你想引用外部函数/符号而不用#include污染全局命名空间的情况下很有用!感谢您指出。我会进行编辑。【参考方案7】:实际上,有一个用例可能很有用。如果你想确保某个函数被调用(并且你的代码可以编译),无论周围的代码声明什么,你都可以打开你自己的块并在其中声明函数原型。 (灵感最初来自 Johannes Schaub,https://***.com/a/929902/3150802,来自 TeKa,https://***.com/a/8821992/3150802)。
如果您必须包含您无法控制的标题,或者如果您有一个可能在未知代码中使用的多行宏,这可能特别有用。
关键是本地声明取代了最里面的封闭块中的先前声明。虽然这可能会引入细微的错误(我认为在 C# 中是禁止的),但可以有意识地使用它。考虑:
// somebody's header
void f();
// your code
int i;
int f(); // your different f()!
i = f();
// ...
链接可能很有趣,因为标头可能属于某个库,但我想您可以调整链接器参数,以便在考虑该库时将f()
解析为您的函数。或者你告诉它忽略重复的符号。或者你不链接到图书馆。
【讨论】:
那么请帮帮我,f
在您的示例中会在哪里定义?我不会因为返回类型不同而导致函数重定义错误吗?
@JonathanMee hmmm... f() 可以在不同的翻译单元中定义,我想。但是,如果您还链接到假定的库,链接器可能会犹豫,我认为您是对的。所以你不能这样做;-),或者至少必须忽略多个定义。
不好的例子。 C++ 中 void f()
和 int f()
之间没有区别,因为函数的返回值不是 C++ 中函数签名的一部分。将第二个声明更改为int f(int)
,我将删除我的反对票。
@DavidHammen 在声明void f()
后尝试编译i = f();
。 “没有区别”只是事实的一半;-)。我实际上使用了不可重载的函数“签名”,否则整个情况在 C++ 中是不必要的,因为具有不同参数类型/编号的两个函数可以愉快地共存。
@DavidHammen 确实,在阅读了 Shafik 的回答后,我相信我们有三种情况:1。签名的参数不同。 在 C++ 中没有问题,简单的重载和最佳匹配规则有效。 2。签名完全没有区别。 语言级别没有问题;通过链接所需的实现来解决功能。 3。差异仅在于返回类型。存在语言级别的问题,如所示;重载决议不起作用;我们必须适当地声明一个具有不同签名和链接的函数。【参考方案8】:
这不是对OP问题的回答,而是对几个cmets的回答。
我不同意 cmets 和答案中的这些观点:1 嵌套声明据称是无害的,2 嵌套定义是无用的。
1 嵌套函数声明无害的主要反例是infamous Most Vexing Parse。 IMO 由它引起的混乱蔓延足以保证一个额外的规则禁止嵌套声明。
2 嵌套函数定义无用的第一个反例是经常需要在一个函数内的多个位置执行相同的操作。有一个明显的解决方法:
private:
inline void bar(int abc)
// Do the repeating operation
public:
void foo()
int a, b, c;
bar(a);
bar(b);
bar(c);
但是,这种解决方案经常会用大量私有函数污染类定义,每个私有函数都只在一个调用者中使用。嵌套函数声明会更干净。
【讨论】:
我认为这很好地总结了我提出问题的动机。如果您查看我引用 MVP 的原始版本,但我一直在 cmets(我自己的问题)中被否决,被告知 MVP 无关紧要:( 我只是无法弄清楚代码声明中的潜在有害因素如何仍然存在,但在代码定义中可能没有用处。我已经为有益的示例给了你一个 +1。【参考方案9】:具体回答这个问题:
从答案看来,代码内声明可能能够防止命名空间污染,但我希望听到的是为什么允许声明函数的能力但不允许定义函数的能力。
因为考虑这段代码:
int main()
int foo()
// Do something
return 0;
return 0;
语言设计师的问题:
-
应该
foo()
可用于其他功能吗?
如果是这样,它应该叫什么名字? int main(void)::foo()
?
(请注意,2 在 C 中是不可能的,C++ 的鼻祖)
如果我们想要一个本地函数,我们已经有办法 - 使其成为本地定义类的静态成员。那么我们是否应该添加另一种达到相同结果的句法方法?为什么要这样做?会不会增加 C++ 编译器开发者的维护负担?
等等……
【讨论】:
显然这种行为是为 lambdas 定义的?为什么不在代码中定义函数? lambda 只是编写函数对象的简写。不捕获参数的 lambda 的特殊情况等价于本地函数定义,就像编写没有数据成员的函数对象一样。 我只是指出代码声明函数中的 lambdas 和 和 已经忽略了您的所有观点。应该不会增加“负担”。 @JonathanMee 如果您对此感觉强烈,请务必向 c++ 标准委员会提交 RFC。 Shafik Yaghmour's Answer 涵盖了已经完成的工作。如果它们不允许我们定义它们,我个人希望看到删除在代码中声明函数的能力。 Richard Hodges's answer 很好地解释了为什么我们仍然需要在代码声明中声明的能力。【参考方案10】:只是想指出,GCC 编译器允许您在函数中声明函数。阅读更多关于它的信息here。同样随着 lambdas 到 C++ 的引入,这个问题现在有点过时了。
在其他函数中声明函数头的能力,我发现在以下情况下很有用:
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);
我们这里有什么?基本上,你有一个应该从 main 调用的函数,所以你要做的就是像往常一样转发声明它。但后来你意识到,这个函数还需要另一个函数来帮助它完成它正在做的事情。因此,不是在 main 上方声明该辅助函数,而是在需要它的函数中声明它,然后可以从该函数调用它,并且只能从该函数调用。
我的观点是,在函数内部声明函数头可以是函数封装的间接方法,它允许函数通过委托给只有它知道的其他函数来隐藏它正在执行的某些部分,几乎 给人一种嵌套函数的错觉。
【讨论】:
我知道我们可以定义一个 lambda inline。我知道我们可以声明一个内联函数,但这就是most vexing parse 的由来,所以我的问题是,如果标准要保留仅用于引起程序员愤怒的功能,程序员不应该能够定义函数也内联? Richard Hodges' answer 帮助我了解了这个问题的根源。【参考方案11】:可能允许嵌套函数声明 1. 前向引用 2. 能够声明指向函数的指针并在有限范围内传递其他函数。
嵌套函数定义是不允许的,可能是由于以下问题 1.优化 2.递归(封闭和嵌套定义的函数) 3. 再入 4.并发和其他多线程访问问题。
根据我有限的理解:)
【讨论】:
以上是关于为啥我不能在另一个函数中定义一个函数?的主要内容,如果未能解决你的问题,请参考以下文章