C++中能否在函数内部定义另一个函数?

360

我的意思是这样的:

int main() 
{
  void a() 
  {
      // code
  }
  a();

  return 0;
}

2
你为什么要尝试做这件事?解释你的目的可能会让其他人告诉你实现目标的正确方法。 - Thomas Owens
8
gcc支持嵌套函数作为非标准扩展,但最好即使在使用gcc时也不要使用它。而且,在C++模式下,它无法使用。 - Sven Marnach
65
@Thomas: 因为减少函数的作用域范围会是个好主意。在其他编程语言中,函数内嵌套函数是常见的特性。 - Johan Kotlinski
95
他正在谈论嵌套函数。就像可以在类中嵌套类一样,他想要在一个函数内部嵌套另一个函数。实际上,我也曾经遇到过这种情况,如果可能的话我也会这么做。有些语言(如F#)允许这样做,我可以告诉您,这可以使代码更清晰、可读性更强、更易于维护,而不会使库中充满了无用的助手函数,这些函数只在非常特定的上下文中有用。;) - Mephane
36
嵌套函数可以非常好地用于将复杂的函数/算法进行拆分,而不必在当前作用域中填充那些在封闭作用域内没有通用用途的函数。在我看来,Pascal和Ada对它们有着可爱的支持。同样,Scala和许多其他古老/新受尊敬的语言也是如此。像任何其他功能一样,它们也可能会被滥用,但这取决于开发人员。在我看来,它们对我们的益处要大于对我们的伤害。 - luis.espinal
显示剩余5条评论
13个回答

470

使用lambda表达式实现现代C++编程

在当前的C++版本(C++11、C++14和C++17)中,您可以通过lambda表达式将函数嵌套在函数内部:

int main() {
    // This declares a lambda, which can be called just like a function
    auto print_message = [](std::string message) 
    { 
        std::cout << message << "\n"; 
    };

    // Prints "Hello!" 10 times
    for(int i = 0; i < 10; i++) {
        print_message("Hello!"); 
    }
}

通过**按引用捕获*,Lambda表达式也可以修改本地变量。使用按引用捕获方式,Lambda表达式可以访问在Lambda表达式范围内声明的所有本地变量。它可以正常地修改和更改这些变量。

int main() {
    int i = 0;
    // Captures i by reference; increments it by one
    auto addOne = [&] () {
        i++; 
    };

    while(i < 10) {
        addOne(); //Add 1 to i
        std::cout << i << "\n";
    }
}

C++98和C++03-不是直接支持,但可以使用局部类中的静态函数实现

C++不直接支持这个功能。

尽管如此,您可以使用局部类,并且它们可以具有函数(非staticstatic),因此您可以在某种程度上实现此目标,尽管这有些笨拙:

int main() // it's int, dammit!
{
  struct X { // struct's as good as class
    static void a()
    {
    }
  };

  X::a();

  return 0;
}

然而,我对这种做法持怀疑态度。每个人都知道(嗯,现在你也知道了:)),C++不支持局部函数,所以他们习惯于没有它们。然而,他们不习惯于那个补救措施。我会花很长时间在这段代码上,确保它只是为了允许局部函数而存在。这不好。


3
如果您对返回类型十分严谨,主函数还需要接受两个参数。 :) (或者现在这是可选的而不是返回类型?我跟不上了。) - Leo Davidson
3
这只是糟糕的 - 它打破了良好、清晰代码的所有约定。我想不出任何一个场景,这是一个好主意。 - Thomas Owens
38
如果你需要一个回调函数,但不想在其他名称空间中污染它,那么使用它是很好的选择。 - Leo Davidson
12
@Leo:这个标准规定了main函数有两种有效的形式:int main()int main(int argc, char* argv[]) - John Dibling
12
标准规定必须支持int main()int main(int argc, char* argv[]),其他可能会被支持,但所有的都必须有返回值 int。 - JoeG
显示剩余20条评论

283

从所有意义上来说,C++支持通过lambda表达式实现这一点:1

int main() {
    auto f = []() { return 42; };
    std::cout << "f() = " << f() << std::endl;
}

在这里,f是一个lambda对象,在main中充当本地函数。可以指定捕获来允许该函数访问本地对象。

在幕后,f是一个函数对象(即一种类型的对象,它提供了一个operator())。函数对象类型是由编译器根据lambda创建的。


1自C++11以来


7
啊,太棒了!我没想到这个。这比我的想法好多了,我给你点赞(+1)。 - sbi
1
@sbi: 实际上我过去曾经使用本地结构体来模拟这个(是的,我对自己感到相当羞愧)。但是它的实用性受限于本地结构体不会创建闭包,即你无法在其中访问本地变量。你需要通过构造函数显式地传递和存储它们。 - Konrad Rudolph
1
@Konrad:它们的另一个问题是,在C++98中,您不能将本地类型用作模板参数。我认为C++1x已经解除了这个限制,不过(或者是C++03吗?) - sbi
3
@luis: 我必须同意Fred的观点。你给lambda赋予了一种它们实际上没有的含义(这不仅适用于C++,也适用于我使用过的其他语言,但不包括Python和Ada)。此外,在C++中做出这种区分并没有意义,因为C++根本没有本地函数(local functions)。它只有lambda表达式。如果你想将类似函数的东西的作用限制在一个函数内部,你唯一的选择是lambda或其他回答中提到的局部结构体。我认为后者过于复杂,没有任何实用价值。 - Konrad Rudolph
2
@AustinWBryan 不,C++中的lambda只是functor的语法糖,没有任何开销。这个网站上有更详细的问题。 - Konrad Rudolph
显示剩余35条评论

54

已经提到了本地类,但以下是一种让它们看起来更像本地函数的方法,使用一个operator()重载和匿名类:

int main() {
    struct {
        unsigned int operator() (unsigned int val) const {
            return val<=1 ? 1 : val*(*this)(val-1);
        }
    } fac;

    std::cout << fac(5) << '\n';
}

我不建议使用这个,它只是一个有趣的技巧(可以做,但我认为不应该)。


2014更新:

随着C++11的崛起,你现在可以拥有本地函数,其语法有点类似于JavaScript:

auto fac = [] (unsigned int val) {
    return val*42;
};

对于递归函数,不支持编译时类型推导:

function<int(int)> factorial{ [&](int n)
{
        return (n == 1 || n == 0) ? 1 : factorial(n - 1) * n;
} };

1
应该是 operator () (unsigned int val),你少了一对括号。 - Joe D
1
实际上,如果您需要将此函数对象传递给STL函数或算法(如std::sort()std::for_each()),那么这是完全合理的事情。 - Dima
1
@Dima:不幸的是,在C++03中,本地定义的类型不能用作模板参数。 C++0x修复了这个问题,但也提供了更好的解决方案,如lambda表达式,因此您仍然不会这样做。 - Ben Voigt
3
递归是被支持的,但是你不能使用auto来声明变量。Stroustrup给出了一个示例:function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };,用于翻转给定起始和结束指针的字符串。 - Eponymous
@Eponymous:感谢您提供的信息,我之前并不知道这个。 - Sebastian Mach
显示剩余6条评论

27
你不能在C++中使用本地函数。但是,C++11有lambda表达式。Lambda表达式基本上是像函数一样工作的变量。

Lambda表达式具有类型std::function(实际上并不完全正确, 但在大多数情况下可以假设它是这样的)。要使用此类型,您需要#include <functional>std::function是一个模板,以返回类型和参数类型为模板参数,语法为std::function<ReturnType(ArgumentTypes)>。例如,std::function<int(std::string, float)>是一个返回int并接受两个参数(一个std::string和一个float)的lambda表达式。最常见的是std::function<void()>,它不返回任何东西并且不带参数。

一旦声明了lambda表达式,就可以像普通函数一样调用它,使用语法lambda(arguments)

要定义一个 lambda 表达式,使用语法[captures](arguments){code} (还有其他方法,但我不会在这里提到)。arguments是 lambda 表达式的参数,code是调用 lambda 时要运行的代码。通常,您可以使用 [=][&] 作为捕获方式。 [=] 表示按值捕获上下文中定义的所有变量的值,这意味着它们将保留在声明 lambda 表达式时的值。 [&] 表示通过引用捕获上下文中的所有变量,这意味着它们始终具有当前值,但如果它们从内存中删除,则程序将崩溃。以下是一些示例:
#include <functional>
#include <iostream>

int main(){
    int x = 1;

    std::function<void()> lambda1 = [=](){
        std::cout << x << std::endl;
    };
    std::function<void()> lambda2 = [&](){
        std::cout << x << std::endl;
    };

    x = 2;
    lambda1();    //Prints 1 since that was the value of x when it was captured and x was captured by value with [=]
    lambda2();    //Prints 2 since that's the current value of x and x was captured by reference with [&]

    std::function<void()> lambda3 = [](){}, lambda4 = [](){};    //I prefer to initialize these since calling an uninitialized lambda is undefined behavior.
                                                                 //[](){} is the empty lambda.

    {
        int y = 3;    //y will be deleted from the memory at the end of this scope
        lambda3 = [=](){
            std::cout << y << endl;
        };
        lambda4 = [&](){
            std::cout << y << endl;
        };
    }

    lambda3();    //Prints 3, since that's the value y had when it was captured

    lambda4();    //Causes the program to crash, since y was captured by reference and y doesn't exist anymore.
                  //This is a bit like if you had a pointer to y which now points nowhere because y has been deleted from the memory.
                  //This is why you should be careful when capturing by reference.

    return 0;
}

您可以通过指定变量名称来捕获特定变量。仅指定名称将按值捕获它们,在名称前加上&将按引用捕获它们。例如,[=,&foo]将按值捕获所有变量,除了foo将被按引用捕获,而[&,foo]将按引用捕获所有变量,除了foo将按值捕获。您还可以仅捕获特定变量,例如[&foo]将按引用捕获foo,并且不会捕获任何其他变量。您还可以使用[]来不捕获任何变量。如果您尝试在lambda中使用未捕获的变量,则无法编译。以下是一个示例:
#include <functional>

int main(){
    int x = 4, y = 5;

    std::function<void(int)> myLambda = [y](int z){
        int xSquare = x * x;    //Compiler error because x wasn't captured
        int ySquare = y * y;    //OK because y was captured
        int zSquare = z * z;    //OK because z is an argument of the lambda
    };

    return 0;
}

你无法更改在lambda内部通过值捕获的变量的值(通过值捕获的变量在lambda内部具有const类型)。要更改它,你需要通过引用来捕获该变量。以下是一个例子:

#include <functional>

int main(){
    int x = 3, y = 5;
    std::function<void()> myLambda = [x, &y](){
        x = 2;    //Compiler error because x is captured by value and so it's of type const int inside the lambda
        y = 2;    //OK because y is captured by reference
    };
    x = 2;    //This is of course OK because we're not inside the lambda
    return 0;
}

此外,调用未初始化的lambda表达式是未定义的行为,通常会导致程序崩溃。例如,永远不要这样做:
std::function<void()> lambda;
lambda();    //Undefined behavior because lambda is uninitialized

例子

这是使用lambda表达式实现你在问题中想要做的代码:

#include <functional>    //Don't forget this, otherwise you won't be able to use the std::function type

int main(){
    std::function<void()> a = [](){
        // code
    }
    a();
    return 0;
}

这是一个更高级的lambda示例:

#include <functional>    //For std::function
#include <iostream>      //For std::cout

int main(){
    int x = 4;
    std::function<float(int)> divideByX = [x](int y){
        return (float)y / (float)x;    //x is a captured variable, y is an argument
    }
    std::cout << divideByX(3) << std::endl;    //Prints 0.75
    return 0;
}

20

不行。

你正在尝试做什么?

解决方法:

int main(void)
{
  struct foo
  {
    void operator()() { int a = 1; }
  };

  foo b;
  b(); // call the operator()

}

2
请注意,类实例化方法需要进行内存分配,因此在效率上不如静态方法。 - ManuelSchneid3r
@ManuelSchneid3r,不是用C++。footrivially default-constructible,尽管没有成员,但它并不具有零大小,因为标准不允许零大小的结构类型,但是除非你将从堆栈指针寄存器中减去一个常数视为“分配”(无论如何,这将被任何明智的编译器在-O1级别上消除),否则它是免费的。我不是说它不丑陋;我的观点是它是没有分配的。 :) - kkm
在我看来,最好将其作为静态函数,然后使用 foo::f()。无论如何,我还是点了赞,因为这绝对是在 C++03 中实现它的最直接的方法。 - Keith Russell

17

从C++11开始,您可以使用适当的lambda表达式。有关更多详细信息,请参阅其他答案。


旧的答案:您可以使用一个虚假类,但需要进行欺骗:

void moo()
{
    class dummy
    {
    public:
         static void a() { printf("I'm in a!\n"); }
    };

    dummy::a();
    dummy::a();
}

除非创建一个对象(在我看来会增加很多噪音),否则我不确定你能做到。除非有一些聪明的命名空间技巧,但我想不出来,而且滥用语言可能不是一个好主意。 :) - Leo Davidson
“getting-rid-of-dummy::” 在其他答案中。 - Sebastian Mach

12

不可以。C和C++默认都不支持该功能,但是TonyK在评论中指出,GNU C编译器有扩展可以在C语言中实现这种行为。


2
它是由GNU C编译器支持的特殊扩展,但仅适用于C语言,不适用于C++。 - TonyK
啊,我的 C 编译器里没有任何特殊的扩展。不过知道这点也挺好的。我会把这个细节添加到我的答案里。 - Thomas Owens
我已经使用了gcc的扩展来支持嵌套函数(在C中,而不是C++)。嵌套函数是一种很棒的东西(就像Pascal和Ada中的那样),用于管理复杂但紧密结构,这些结构不适用于一般用途。只要使用gcc工具链,它几乎可以在所有目标架构上移植。但如果有必要使用非gcc编译器编译生成的代码,则最好避免使用此类扩展,并尽可能接近ansi/posix原则。 - luis.espinal

9

在C++中,您无法在另一个函数内定义自由函数。


2
不是使用ansi/posix,但你可以使用gnu扩展。 - luis.espinal

9

正如其他人所提到的,您可以使用gcc中的gnu语言扩展来使用嵌套函数。如果您(或您的项目)坚持使用gcc工具链,则您的代码将在gcc编译器针对的不同体系结构上大多数可移植。

但是,如果可能需要使用不同的工具链编译代码,则我会避免使用此类扩展。


在使用嵌套函数时,也要小心。它们是管理复杂而紧密代码块结构的绝佳解决方案(这些代码块的各个部分并不用于外部/通用用途)。它们还非常有助于控制命名空间污染(在冗长的语言和自然复杂/长类的情况下,这是一个非常现实的问题)。

但同样,它们也可能被滥用。

很遗憾,C/C++不支持此类特性作为标准。大多数Pascal变体和Ada都支持(几乎所有Algol-based语言都支持)。JavaScript也是一样。现代语言Scala也是如此。尊贵的语言Erlang、Lisp或Python也是如此。

就像C/C++一样,不幸的是,Java(我赚取大部分收入的语言)也不支持。

我在这里提到Java,因为我看到有几个海报建议使用类和类的方法作为嵌套函数的替代方案。这也是Java中的典型解决方法。

简短的回答:不行。

这样做往往会在类层次结构上引入人为和不必要的复杂性。所有事情都相等的情况下,理想情况是使表示实际领域的类层次结构(及其包含的命名空间和范围)尽可能简单。

嵌套函数有助于处理“私有”的内部功能。如果缺少这些设施,则应尝试避免将该“私有”复杂性传播到类模型中。

在软件(以及任何工程学科)中,建模是一种权衡。因此,在现实生活中,将有合理的例外情况(或者更确切地说是指南)。但是要小心。


8

所有这些技巧看起来(或多或少)像本地函数,但它们并不像本地函数那样工作。在本地函数中,您可以使用其超级函数的局部变量。它是一种半全局变量。这些技巧中没有一个能做到这一点。最接近的是c++0x中的lambda技巧,但它的闭包在定义时间绑定,而不是使用时间。


现在我认为这是最好的答案。虽然可以在函数内部声明一个函数(我经常使用),但它不是许多其他语言中定义的本地函数。了解这种可能性仍然是很好的。 - Alexis Wilke

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