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


80

这不是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是函数定义,其他两个是声明
一些程序员哥们2015年

9
我认为您以错误的方式获得了这些术语-您想问“允许我们声明而不在代码内部定义函数的意义是什么?”。而当我们使用它时,您可能会说“在函数内部”。都是“代码”。
彼得-恢复莫妮卡2015年

14
如果您想问一问这种语言为什么会有古怪和不一致的地方:因为它是几十年来发展起来的,通过许多人的不同想法,他们是出于不同原因在不同时期发明的。如果您要问为什么它有这个特殊的怪癖:因为到目前为止,还没有人认为本地函数定义足以标准化。
Mike Seymour 2015年

4
@MikeSeymour正确正确。C的结构不如Pascal好,并且始终只允许顶层函数定义。因此,原因是历史性的,再加上无需进行更改的必要。函数声明是可能的,这仅是范围声明通常可能的结果。禁止功能将意味着额外的规则。
彼得-恢复莫妮卡2015年

3
@JonathanMee:大概是因为通常在块中允许声明,并且没有特别的理由专门禁止函数声明;只允许不带特殊情况的任何声明都更简单。但是“为什么”并不是一个真正可以回答的问题。语言之所以如此,是因为它就是这样发展的。
Mike Seymour 2015年

Answers:


41

目前尚不清楚为什么one不允许这样做。嵌套函数是很久以前在N0295中提出的,它表示:

我们讨论了将嵌套函数引入C ++。嵌套函数已广为人知,并且它们的引入不需要编译器供应商,程序员或委员会的任何努力。嵌套函数提供了显着的优势,[...]

显然,该提议已被拒绝,但是由于我们没有在线会议记录,因为我们没有获得拒绝1993理由的可能来源。

实际上,此建议在Lambda表达式和C ++闭包中已作为一种可能的替代方法进行了说明:

一篇[Bre88]和向C ++委员会[SH93]提出的建议N0295建议向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);应该致电给您所需的电话g
彼得-恢复莫妮卡2015年

@PeterSchneider我在这里说的太过分了,我对此进行了调整。
Shafik Yaghmour

1
我想在此处添加评论:接受此答案不是因为它能最好地解释为什么允许在代码中声明函数;但是因为它在描述为什么不允许在代码函数定义中做得最好,这才是实际的问题。特别是,它专门概述了为什么在代码函数中的假设实现会与lambda的实现不同。+1
乔纳森·梅

1
@JonathanMee:世界上的情况如何:“ ...我们没有这种拒绝理由的可能来源。” 是描述为什么不允许嵌套函数定义(甚至根本不想描述嵌套函数)的最佳工作
杰里·科芬

@JerryCoffin答案包括正式的理由,为什么lambdas已经是代码函数定义中的超集,从而使它们的实现变得不必要:“除非该函数处于活动状态,否则无法使用所产生的闭包...而且,这些提案也无法复制局部变量转换成闭包。” 我以为您是在问为什么对编译器所带来的额外复杂性的分析不是我接受的答案。如果是这样:您说了lambda已经完成的困难,那么代码定义显然可以像lambda一样实现。
乔纳森·梅

31

好吧,答案是“历史原因”。在C中,您可以在块范围内进行函数声明,而C ++设计人员看不到删除该选项的好处。

一个示例用法是:

#include <iostream>

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

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

IMO这是一个坏主意,因为通过提供与函数的实际定义不匹配的声明很容易犯错,从而导致未定义的行为,编译器将无法诊断该行为。


10
“这通常被认为是一个坏主意” -需要引用。
理查德·霍奇斯

4
@RichardHodges:函数声明属于头文件,而实现则属于.c或.cpp文件,因此在函数定义中包含这些声明违反了这两个准则。
MSalters 2015年

2
如何防止声明与定义不同?
理查德·霍奇斯

1
@JonathanMee:我是说,如果您使用的声明在定义函数的地方不可用,则编译器可能不会检查该声明是否与定义匹配。因此,您可能有一个本地声明some_type f();,并且在另一个翻译单元中有一个定义another_type f() {...}。编译器无法告诉您这些不匹配,并且f使用错误的声明进行调用将产生未定义的行为。因此,最好在标头中仅包含一个声明,并在定义函数的位置以及使用该标头的地方包含该标头。
Mike Seymour 2015年

6
我认为您的意思是将函数声明放在头文件中的通常做法通常很有用。我认为没有人会不同意这一点。我认为没有理由断言在函数范围内声明外部函数“通常被认为是一个坏主意”。
理查德·霍奇斯

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。


会不会two也有文件,从而导致污染反正在来定义?
乔纳森·梅

1
@JonathanMee不,two()可以在完全不同的编译单元中定义。
理查德·霍奇斯

我需要帮助,以了解这将如何工作。您是否不必包含在其中声明的标头?在什么时候会宣布,对不对?我只是看不到如何在代码中定义它,而以某种方式不包含声明它的文件?
乔纳森·梅

5
@JonathanMee标头没有什么特别的。它们只是放置声明的便捷位置。函数中的声明与标头中的声明一样有效。因此,不,您不需要包含要链接的内容的标头(甚至根本没有标头)。
立方

1
@JonathanMee在C / C ++语言中,定义和实现是同一回事。您可以根据需要多次声明一个函数,但是只能定义一次。声明不必位于以.h结尾的文件中-您可以拥有一个use.cpp文件,该文件具有一个调用foo的功能栏(在其主体中声明foo),而文件providers.cpp定义了foo,只要您不弄乱链接步骤,它就可以正常工作。
立方

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只需在前一个堆栈指针处查找堆栈即可访问foo的变量。对?

错误!好吧,在某些情况下这可能是正确的,但不一定如此。特别是,bar可以是递归的,在这种情况下,bar可能必须查看一些几乎任意数量的级别来备份堆栈,以查找周围函数的变量。一般来说,您需要执行以下两项操作之一:将一些额外的数据放在堆栈上,以便它可以在运行时在堆栈上进行搜索以查找其周围函数的堆栈框架,或者有效地将指针传递给周围函数的堆栈框架作为嵌套函数的隐藏参数。哦,但是不一定也有一个周围的函数-如果可以嵌套函数,则可以将它们嵌套(或多或少)任意深度,因此您需要准备传递任意数量的隐藏参数。这意味着您通常最终会得到一些堆栈帧到周围函数的链接列表,

但是,这意味着访问“局部”变量可能不是一件容易的事。找到正确的堆栈框架来访问变量可能并非易事,因此(至少通常)访问周围函数的变量要比访问真正的局部变量慢。而且,当然,编译器必须生成代码以找到正确的堆栈帧,通过任意数量的堆栈帧中的任何一个访问变量,等等。

是C通过禁止嵌套函数避免的复杂性。现在,可以肯定的是,当前的C ++编译器与1970年代的老式C编译器完全不同。对于诸如多重,虚拟继承之类的事情,C ++编译器在任何情况下都必须以相同的一般性质来处理事情(即,在这种情况下查找基类变量的位置也很重要)。从百分比的角度来看,支持嵌套函数不会给当前的C ++编译器增加太多复杂性(有些已经支持它们了,例如gcc)。

同时,它也很少增加实用性。特别是,如果你想定义的东西,行为像一个函数的功能里面,你可以使用lambda表达式。这实际上创建的是一个对象(即某个类的实例),该对象使函数调用运算符(operator())重载,但仍提供类似函数的功能。但是,它使从周围环境中捕获(或不捕获)数据更加明确,这使其可以使用现有机制,而不是发明一种全新的机制和使用规则。

底线:尽管一开始看起来嵌套声明很困难并且嵌套函数很琐碎,但事实恰恰相反:嵌套函数实际上比嵌套声明要复杂得多。


5

第一个是函数定义,不允许使用。显然,wt是将一个函数的定义放在另一个函数中的用法。

但是其他两个只是声明。假设您需要int two(int bar);在main方法内部使用函数。但是它是在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
什么是“减速”?您是说“声明”吗?
彼得·莫滕森

4

此语言功能是从C继承的,在C的早期它就起到了一定的作用(也许是函数声明作用域?)。我不知道现代C程序员是否经常使用此功能,我对此表示怀疑。

所以,总结一下答案:

现代C ++中(至少我知道)没有此功能的目的,这是因为C ++到C的向后兼容(我想是:)。


感谢以下评论:

函数原型的作用域是声明它的函数,因此可以使用一个整齐的全局名称空间-通过引用外部函数/符号而无需使用#include


目的是控制名称的范围,以避免全局命名空间污染。
理查德·霍奇斯

好的,我想这在某些情况下很有用,当您想引用外部函数/符号而不用#include污染全局名称空间时!感谢您指出。我将进行编辑。
mr.pd 2015年

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()在考虑库时将其解析为函数。或者您告诉它忽略重复的符号。否则您不会链接到库。


因此,在这里帮助我f,您的示例将在哪里定义?由于这些错误仅在返回类型上有所不同,我是否最终不会遇到函数重新定义错误?
乔纳森·梅

我认为@JonathanMee hmmm ... f()可以在其他翻译单元中定义。但是,如果您也链接到假定的库,则链接器可能会不知所措。因此,您不能这样做;-),或者至少必须忽略多个定义。
彼得-恢复莫妮卡2015年

不好的例子。C ++void f()int f()C ++之间没有区别,因为函数的返回值不是C ++中函数签名的一部分。将第二个声明更改为int f(int),我将删除我的弃权票。
David Hammen

@DavidHammeni = f();声明后尝试编译void f()。“没有区别”只是事实的一半;-)。我实际上使用了不可重载的函数“签名”,因为否则否则整个情况在C ++中将是不必要的,因为两个具有不同参数类型/数字的函数可以愉快地共存。
彼得-恢复莫妮卡

@DavidHammen实际上,在阅读Shafik的答案后,我相信我们有以下三种情况:1.签名的参数不同。在C ++中没有问题,简单的重载和最佳匹配规则就可以工作。2.签名完全没有区别。语言层面没有问题;通过链接所需的实现来解决功能。3.区别仅在于返回类型。还有就是在语言水平的问题,这表现; 重载解析不起作用;我们必须声明一个具有不同签名的函数 正确链接。
彼得-恢复莫妮卡2015年

3

这不是对OP问题的答案,而是对若干评论的答复。

我在评论和回答中不同意这些观点:1嵌套声明据称是无害的,2嵌套定义是无用的。

1所谓的嵌套函数声明的无害性的主要反例是臭名昭著的Most Vexing Parse。国际海事组织由此引起的混乱蔓延足以保证有一条额外的规则禁止嵌套声明。

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无关紧要:(我只是无法弄清楚代码声明中潜在的危害仍在这里,但潜在有用的代码定义不是我给你有益的例子+1。
乔纳森眉

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定义的吗?为什么不用代码定义函数?
乔纳森·梅

Lambda仅仅是编写功能对象的简写。不捕获任何参数的lambda的特殊情况等同于本地函数定义,就像编写没有数据成员的函数对象一样。
理查德·霍奇斯

我刚刚指出的是lambda,并且在代码中声明的函数已经消除了您的所有观点。不应增加“负担”。
乔纳森·梅

@JonathanMee如果您对此有强烈的想法,请务必将RFC提交给c ++标准委员会。
理查德·霍奇斯

Shafik Yaghmour的答案涵盖了已经完成的工作。我个人希望看到如果不能让我们定义函数,则可以取消在代码中声明函数的功能。Richard Hodges的答案很好地解释了为什么我们仍然需要在代码声明中声明的能力。
乔纳森·梅

1

只是想指出,GCC编译器允许您在函数内部声明函数。在此处了解更多信息。另外,随着将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之上声明该helper函数,不如在需要它的函数中声明它,然后可以从该函数调用该函数,并且只能从该函数调用它。

我的观点是,在函数内部声明函数标头可以是函数封装的间接方法,该方法允许函数通过委派给只有它自己知道的其他函数来隐藏其工作的某些部分,这几乎给人一种嵌套的错觉功能


我知道我们可以定义一个lambda内联。我知道我们可以声明一个内联函数,但这是最令人头疼的解析器的起源,所以我的问题是,该标准是否要保留仅在程序员中引起轰动的功能,程序员是否不能定义该函数?功能内联了吗?理查德·霍奇斯(Richard Hodges)的回答帮助我了解了此问题的根源。
乔纳森·梅

0

嵌套函数声明可能允许用于1.前向引用2.为了能够声明指向函数的指针并在有限范围内传递其他函数。

可能由于诸如1.优化2.递归(封闭和嵌套的已定义函数)之类的问题而导致嵌套函数定义不被允许。3.重入4.并发和其他多线程访问问题。

据我有限的理解:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.