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

Why can't I define a function inside another function?

这不是 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?

编辑:

从答案来看,代码内声明似乎可以防止命名空间污染,但我希望听到的是为什么允许声明函数但不允许定义函数的能力.

嗯,答案是"historical reasons"。在 C 中,您可以在块作用域中声明函数,而 C++ 设计者没有看到删除该选项的好处。

示例用法为:

#include <iostream>

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

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

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

这个语言特性是从 C 继承而来的,它在 C 的早期起到了一定的作用(也许是函数声明作用域?)。 我不知道这个特性是否被现代 C 程序员使用得很多,我真诚地怀疑它。

所以,总结一下答案:

modern C++(至少我知道)中没有此功能的用途,因为 C++ 到 C 的向后兼容性(我想:)).


感谢以下评论:

函数原型的作用域是它在其中声明的函数,因此可以拥有更整洁的全局命名空间 - 通过引用外部 functions/symbols 而无需 #include

在您给出的示例中,void two(int) 被声明为外部函数,该声明 仅在 main 函数的范围内有效.

如果您只想使名称 twomain() 内可用,以避免污染当前编译单元内的全局命名空间,这是合理的。

回复评论的例子:

main.cpp:

int main() {
  int foo();
  return foo();
}

foo.cpp:

int foo() {
  return 0;
}

不需要头文件。编译并 link 与

c++ main.cpp foo.cpp 

它将编译并 运行,程序将按预期 return 0。

第一个是函数定义,不允许。很明显,wt是把一个函数的定义放在另一个函数里面的用法

但其他两个只是声明。假设您需要在 main 方法中使用 int two(int bar); 函数。但它是在 main() 函数下面定义的,因此函数内部的函数声明使您可以通过声明使用该函数。

第三个也是如此。函数内的 Class 声明允许您在函数内使用 class 而无需提供适当的 header 或引用。

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;
}

实际上,有一个用例可以想象是有用的。如果你想确保调用某个函数(并且你的代码编译),无论周围的代码声明什么,你都可以打开你自己的块并在其中声明函数原型。 (灵感最初来自Johannes Schaub,https://whosebug.com/a/929902/3150802, via TeKa, https://whosebug.com/a/8821992/3150802)。

如果您必须包含您无法控制的 headers,或者如果您有一个可能在未知代码中使用的 multi-line 宏,这可能特别有用。

关键是局部声明取代了最里面的封闭块中的先前声明。虽然这会引入细微的错误(我认为这在 C# 中是被禁止的),但可以有意识地使用它。考虑:

// somebody's header
void f();

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

链接可能很有趣,因为 headers 可能属于一个库,但我想您可以调整 linker 参数,以便 f() 解析为您的函数图书馆被考虑的时间。或者你告诉它忽略重复的符号。或者你不link反对图书馆。

具体回答这个问题:

From the answers it seems that there in-code declaration may be able to prevent namespace pollution, what I was hoping to hear though is why the ability to declare functions has been allowed but the ability to define functions has been disallowed.

因为考虑这个代码:

int main()
{
  int foo() {

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

语言设计者的问题:

  1. foo() 是否可以用于其他功能?
  2. 如果有,它的名字应该是什么? int main(void)::foo()?
  3. (注意2在C++的鼻祖C里是不可能的)
  4. 如果我们想要一个局部函数,我们已经有办法 - 使它成为局部定义的静态成员 class。那么我们是否应该添加另一种语法方法来达到相同的结果呢?为什么要这样做?不会增加C++编译器开发者的维护负担吧?
  5. 等等...

你可以做这些事情,主要是因为它们实际上并没有那么难做。

从编译器的角度来看,在另一个函数中声明一个函数很容易实现。编译器需要一种机制来允许函数内部的声明处理函数内部的其他声明(例如,int x;)。

它通常具有解析声明的通用机制。对于编写编译器的人来说,在另一个函数内部或外部解析代码时是否调用该机制根本无关紧要——它只是一个声明,所以当它看到足够多的东西时,就知道有一个声明,它调用编译器处理声明的部分。

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

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

int foo() { 
    int x;

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

没有局部函数,访问局部变量的代码相当简单。在一个典型的实现中,当执行进入函数时,一些用于局部变量的 space 块被分配在堆栈上。所有局部变量都分配在该单个块中,并且每个变量都被视为距块开头(或结尾)的简单偏移量。例如,让我们考虑这样一个函数:

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 调用可能必须查看堆栈中几乎任意数量的级别,以找到周围函数的变量。一般来说,您需要做以下两件事之一:要么在堆栈上放置一些额外的数据,以便它可以在 运行-time 搜索堆栈以找到其周围函数的堆栈帧,或者您有效地将指向周围函数堆栈帧的指针作为隐藏参数传递给嵌套函数。哦,但也不一定只有一个外围函数——如果你可以嵌套函数,你可能可以将它们嵌套(或多或少)任意深度,所以你需要准备好传递任意数量的隐藏参数。这意味着您通常会得到一些类似于周围函数的堆栈帧的链接列表,并且通过遍历该链接列表以找到其堆栈指针,然后访问该堆栈指针的偏移量来访问周围函数的变量。

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

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

同时,它也很少增加实用性。特别是,如果您想在函数内部定义一些 表现得像 的函数,您可以使用 lambda 表达式。这实际上创建的是一个重载函数调用运算符 (operator()) 的对象(即某些 class 的实例),但它仍然提供类似函数的功能。它使得从周围上下文中捕获(或不捕获)数据更加明确,这允许它使用现有机制而不是发明一个全新的机制和一套规则供其使用。

底线:尽管最初看起来嵌套声明很难,嵌套函数很简单,但或多或​​少恰恰相反:嵌套函数实际上比嵌套声明更复杂。

为什么不允许 one 并不明显;嵌套函数是很久以前在 N0295 中提出的,它说:

We discuss the introduction of nested functions into C++. Nested functions are well understood and their introduction requires little effort from either compiler vendors, programmers, or the committee. Nested functions offer significant advantages, [...]

显然这个提案被拒绝了,但由于我们没有在线可用的 1993 会议记录,我们没有可能的来源来说明拒绝的理由。

事实上,这个提议在 C 的 Lambda 表达式和闭包中有所记录 ++ 作为一个可能的选择:

One article [Bre88] and proposal N0295 to the C ++ committee [SH93] suggest adding nested functions to C ++ . Nested functions are similar to lambda expressions, but are defined as statements within a function body, and the resulting closure cannot be used unless that function is active. These proposals also do not include adding a new type for each lambda expression, but instead implementing them more like normal functions, including allowing a special kind of function pointer to refer to them. Both of these proposals predate the addition of templates to C ++ , and so do not mention the use of nested functions in combination with generic algorithms. Also, these proposals have no way to copy local variables into a closure, and so the nested functions they produce are completely unusable outside their enclosing function

考虑到我们现在有了 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)
}

这不是对 OP 问题的回答,而是对几条评论的回复。

我不同意评论和回答中的这些观点: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);
}

但是,此解决方案通常会用大量私有函数污染 class 定义,每个私有函数仅在一个调用方中使用。嵌套的函数声明会更简洁。

只是想指出 GCC 编译器允许您在函数内部声明函数。详细阅读 here. Also with the introduction of 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 之上声明辅助函数,不如在需要它的函数中声明它,然后它可以从该函数中调用,并且只能从该函数中调用。

我的意思是,在函数内部声明函数头可以是函数封装的一种间接方法,它允许函数通过委托给只有它知道的其他函数来隐藏它正在做的某些部分,几乎给人一种嵌套函数的错觉

可能允许嵌套函数声明 1.转发参考 2. 能够声明指向函数的指针并在有限范围内传递其他函数。

不允许嵌套函数定义可能是由于诸如此类的问题 1.优化 2.递归(封闭和嵌套定义的函数) 3. 重新进入 4. 并发等多线程访问问题

以我有限的理解:)