这不是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
?
修改
从答案看来,代码内声明似乎可以防止命名空间污染,我希望听到的是为什么允许声明函数的能力但是不允许定义函数的能力。< / p>
答案 0 :(得分:40)
为什么不允许one
并不明显;很久以前在N0295中提出了嵌套函数,其中说:
我们讨论将嵌套函数引入C ++。嵌套 功能很好理解,它们的引入几乎不需要 来自编译器供应商,程序员或委员会的努力。 嵌套函数提供了显着的优势,[...]
显然此提案遭到拒绝,但由于1993
我们没有在线提供的会议记录,因此我们没有可能的拒绝理由来源。
事实上,此提案在 Lambda expressions and closures for C ++ 中作为可能的备选方案注明:
一篇文章[Bre88]和提案N0295给C ++委员会[SH93]建议在C中添加嵌套函数 ++。嵌套函数类似于lambda表达式,但是被定义为函数体内的语句,以及结果 除非该函数处于活动状态,否则不能使用闭包。这些建议 也不包括为每个lambda表达式添加新类型,但是 而是更像普通的功能,包括 允许一种特殊的函数指针来引用它们。两者的 这些提议早于向C添加模板 ++,所以不要提及嵌套函数与通用算法的结合使用。此外,这些提案无法复制 局部变量变成了一个闭包,所以嵌套函数就这样了 产品在其封闭功能之外完全无法使用
考虑到我们现在有lambdas,我们不太可能看到嵌套函数,因为正如文章所述,它们是同一问题的替代品,嵌套函数相对于lambdas有几个限制。
至于你问题的这一部分:
// 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 :(得分:31)
嗯,答案是“历史原因”。在C中,您可以在块作用域中具有函数声明,并且C ++设计者没有看到删除该选项的好处。
示例用法是:
#include <iostream>
int main()
{
int func();
func();
}
int func()
{
std::cout << "Hello\n";
}
IMO这是一个坏主意,因为通过提供与函数的真实定义不匹配的声明很容易出错,从而导致编译器无法诊断的未定义行为。
答案 2 :(得分:23)
在您给出的示例中,void two(int)
被声明为外部函数,该声明仅在main
函数的范围内有效。
如果您只希望在two
中使名称main()
可用,那么这是合理的,以避免在当前编译单元中污染全局命名空间。
回应评论的示例:
main.cpp中:
int main() {
int foo();
return foo();
}
Foo.cpp中:
int foo() {
return 0;
}
不需要头文件。编译并链接
c++ main.cpp foo.cpp
它将编译并运行,程序将按预期返回0.
答案 3 :(得分:18)
你可以做这些事情,主要是因为他们实际上并不是那么难做。
从编译器的角度来看,在另一个函数内部使用函数声明实现起来非常简单。编译器需要一种机制来允许函数内部的声明来处理函数内的其他声明(例如,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);
特别是,它有一个位置指向局部变量块的开头,并且对局部变量的所有访问都是从该位置的偏移量。
对于嵌套函数,不再是这种情况 - 相反,函数不仅可以访问自己的局部变量,还可以访问嵌套它所有函数的局部变量。而不是只有一个&#34; stack_pointer&#34;从中计算偏移量,它需要向上走回堆栈以找到它嵌套的函数的本地stack_pointers。
现在,在一个微不足道的情况下,如果bar
嵌套在foo
内,那么bar
就可以查看堆栈了前一个堆栈指针,用于访问foo
个变量。正确?
错误!嗯,有些情况可能是真的,但事实并非如此。特别地,bar
可以是递归的,在这种情况下,bar
的给定调用可能必须看起来几乎任意数量的级别备份堆栈以找到周围函数的变量。一般来说,你需要做两件事之一:要么在堆栈上放一些额外的数据,所以它可以在运行时搜索堆栈以查找其周围函数的堆栈帧,否则你有效将指针传递给周围函数的堆栈帧作为嵌套函数的隐藏参数。哦,但是不一定只有一个周围的函数 - 如果你可以嵌套函数,你可以任意嵌套它们(或多或少),所以你需要准备传递任意数量的隐藏参数。这意味着你通常最终会得到类似于堆栈帧的链接列表到周围函数的内容,并且通过遍历链接列表来查找其堆栈指针,然后从该堆栈指针访问偏移量来访问周围函数的变量。
但是,这意味着可以访问&#34; local&#34;变量可能不是一件小事。找到正确的堆栈帧来访问变量可能非常重要,因此访问周围函数的变量也(至少通常)比访问真正的局部变量更慢。当然,编译器必须生成代码以找到正确的堆栈帧,通过任意数量的堆栈帧访问变量,等等。
这个是C通过禁止嵌套函数避免的复杂性。现在,当前的C ++编译器与1970年代的老式C编译器完全不同,这当然是正确的。对于像多个虚拟继承这样的东西,C ++编译器必须在任何情况下处理这种相同的一般性质的东西(即,在这种情况下找到基类变量的位置也可以是非平凡的)。在百分比的基础上,支持嵌套函数不会给当前的C ++编译器增加太多的复杂性(有些例如gcc已经支持它们)。
同时,它很少增加很多实用性。特别是,如果要像函数内部的函数一样定义 act 的内容,可以使用lambda表达式。这实际上创建的是一个对象(即某个类的实例),它重载了函数调用操作符(operator()
),但它仍然提供了类似函数的功能。它使得从周围环境中捕获(或不捕获)数据更加明确,这使得它可以使用现有机制,而不是发明一个全新的机制和一套规则供其使用。
底线:尽管最初看起来似乎嵌套声明很难并且嵌套函数很简单,但或多或少相反:嵌套函数实际上比嵌套声明要复杂得多。
答案 4 :(得分: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;
}
答案 5 :(得分:4)
这个语言特性是从C继承而来的,它在C早期的某些用途(功能声明范围可能?)。 我不知道现代C程序员是否使用了这个功能,我真诚地怀疑它。
所以,总结一下答案:
现代 C ++中没有这个功能的目的(至少我知道),这是因为C ++ - to-C向后兼容(我想:))。 / p>
感谢以下评论:
函数原型的作用域是声明它的函数,所以可以有一个更整洁的全局命名空间 - 通过引用没有#include
的外部函数/符号。
答案 6 :(得分:4)
实际上,有一个用例可以说是有用的。如果你想确保调用某个函数(并且你的代码编译),无论周围的代码声明什么,你都可以打开自己的块并在其中声明函数原型。 (灵感来自Johannes Schaub,https://stackoverflow.com/a/929902/3150802,通过TeKa,https://stackoverflow.com/a/8821992/3150802)。
如果您必须包含您无法控制的标头,或者您有一个可能在未知代码中使用的多行宏,则这可能特别有用。
关键是本地声明取代最内层封闭块中的先前声明。虽然这可以引入微妙的错误(我认为,在C#中是禁止的),但它可以有意识地使用。考虑:
// somebody's header
void f();
// your code
{ int i;
int f(); // your different f()!
i = f();
// ...
}
链接可能很有趣,因为标题可能属于库,但我想你可以调整链接器参数,以便在考虑库时将f()
解析为你的函数。或者你告诉它忽略重复的符号。或者你没有链接到图书馆。
答案 7 :(得分:2)
具体回答这个问题:
从答案看来,代码内声明似乎可以防止命名空间污染,我希望听到的是为什么允许声明函数的能力但是不允许定义函数的能力。< / p>
因为请考虑以下代码:
int main()
{
int foo() {
// Do something
return 0;
}
return 0;
}
语言设计师的问题:
foo()
是否可以使用其他功能?int main(void)::foo()
?答案 8 :(得分:1)
只是想指出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辅助函数声明为main,而是在需要它的函数内声明它,然后可以从该函数和该函数调用它。
我的观点是,在函数内声明函数头可以是函数封装的间接方法,它允许函数通过委托给其他只有它知道的函数来隐藏它所做的事情的某些部分,几乎给出了嵌套函数的错觉。
答案 9 :(得分:0)
允许使用嵌套函数声明 1.前向参考 2.能够声明一个指向函数的指针并在有限的范围内传递其他函数。
不允许嵌套函数定义可能是由于类似的问题 1.优化 2.递归(封闭和嵌套定义的函数) 3.重新进入 4.并发和其他多线程访问问题。
从我有限的理解:)