Chúng ta có thể có các chức năng bên trong các chức năng trong C ++ không?


225

Ý tôi là đại loại như:

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

  return 0;
}

1
Tại sao bạn cố gắng làm điều này? Giải thích mục đích của bạn có thể cho phép ai đó nói với bạn cách đúng để đạt được mục tiêu của bạn.
Thomas Owens

3
gcc hỗ trợ các hàm lồng nhau như một phần mở rộng không chuẩn. Nhưng tốt hơn đừng sử dụng nó ngay cả khi bạn đang sử dụng gcc. Và trong chế độ C ++, dù sao nó cũng không có sẵn.
Sven Marnach

27
@Thomas: Bởi vì sẽ tốt khi giảm phạm vi của a? Chức năng trong các chức năng là một tính năng thông thường trong các ngôn ngữ khác.
Johan Kotlinski

64
Anh ấy đang nói về các chức năng lồng nhau. Tương tự như việc có thể đến các lớp tiếp theo bên trong các lớp, anh ta muốn lồng một hàm bên trong một hàm. Trên thực tế, tôi cũng đã có những tình huống mà tôi cũng sẽ làm như vậy, nếu có thể. Có những ngôn ngữ (ví dụ F #) cho phép điều này và tôi có thể nói với bạn rằng nó có thể làm cho mã rõ ràng hơn, dễ đọc và dễ bảo trì hơn mà không làm ô nhiễm một thư viện với hàng tá chức năng trợ giúp vô dụng bên ngoài bối cảnh rất cụ thể. ;)
Mephane

16
@Thomas - Các hàm lồng nhau có thể là một cơ chế tuyệt vời để phá vỡ các hàm / thuật toán phức tạp không cần điền vào phạm vi hiện tại với các hàm không được sử dụng chung trong phạm vi kèm theo. Pascal và Ada có (IMO) hỗ trợ đáng yêu cho họ. Tương tự với Scala và nhiều ngôn ngữ cũ / mới được tôn trọng khác. Giống như bất kỳ tính năng nào khác, chúng cũng có thể bị lạm dụng, nhưng đó là chức năng của nhà phát triển. IMO, họ đã có lợi hơn nhiều mà gây bất lợi.
luis.espinal

Câu trả lời:


271

C ++ hiện đại - Có với lambdas!

Trong các phiên bản hiện tại của c ++ (C ++ 11, C ++ 14 và C ++ 17), bạn có thể có các chức năng bên trong các chức năng dưới dạng 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!"); 
    }
}

Lambdas cũng có thể sửa đổi các biến cục bộ thông qua ** Capture-by-Reference *. Với tham chiếu bắt giữ, lambda có quyền truy cập vào tất cả các biến cục bộ được khai báo trong phạm vi của lambda. Nó có thể sửa đổi và thay đổi chúng bình thường.

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 và C ++ 03 - Không trực tiếp, nhưng có với các hàm tĩnh bên trong các lớp cục bộ

C ++ không hỗ trợ trực tiếp.

Điều đó nói rằng, bạn có thể có các lớp cục bộ và chúng có thể có các hàm (không statichoặc static), vì vậy bạn có thể đưa nó đến một số phần mở rộng, mặc dù đó là một chút của một loại bùn:

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

  X::a();

  return 0;
}

Tuy nhiên, tôi sẽ hỏi những lời khen ngợi. Mọi người đều biết (dù sao thì bây giờ bạn cũng vậy :)) C ++ không hỗ trợ các chức năng cục bộ, vì vậy họ đã quen với việc không có chúng. Chúng không được sử dụng, tuy nhiên, để loại bỏ bùn đó. Tôi sẽ dành khá nhiều thời gian cho mã này để đảm bảo rằng nó thực sự chỉ ở đó để cho phép các chức năng cục bộ. Không tốt.


3
Main cũng mất hai đối số nếu bạn định nói về kiểu trả về. :) (Hoặc là tùy chọn nhưng không phải là sự trở lại trong những ngày này? Tôi không thể theo kịp.)
Leo Davidson

3
Điều này chỉ là xấu - nó phá vỡ mọi quy ước về mã tốt, sạch. Tôi không thể nghĩ về một trường hợp duy nhất mà đây là một ý tưởng tốt.
Thomas Owens

19
@Thomas Owens: Thật tốt nếu bạn cần một hàm gọi lại và không muốn làm ô nhiễm một số không gian tên khác với nó.
Leo Davidson

9
@Leo: Tiêu chuẩn cho biết có hai hình thức cho phép chính: int main()int main(int argc, char* argv[])
John Dibling

8
Tiêu chuẩn nói int main()int main(int argc, char* argv[])phải được hỗ trợ và những người khác có thể được hỗ trợ nhưng tất cả họ đều có int.
JoeG

260

Đối với tất cả ý định và mục đích, C ++ hỗ trợ điều này thông qua lambdas : 1

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

Ở đây, flà một đối tượng lambda hoạt động như một hàm cục bộ main. Chụp có thể được chỉ định để cho phép chức năng truy cập các đối tượng cục bộ.

Đằng sau hậu trường, flà một đối tượng chức năng (tức là một đối tượng thuộc loại cung cấp một operator()). Kiểu đối tượng hàm được tạo bởi trình biên dịch dựa trên lambda.


1 kể từ C ++ 11


5
Ah, thật là gọn gàng! Tôi đã không nghĩ về nó. Điều này tốt hơn nhiều so với ý tưởng của tôi, +1từ tôi.
sbi

1
@sbi: Tôi thực sự đã sử dụng các cấu trúc địa phương để mô phỏng điều này trong quá khứ (vâng, tôi rất xấu hổ về bản thân mình). Nhưng tính hữu dụng bị hạn chế bởi thực tế là các cấu trúc cục bộ không tạo ra một bao đóng, tức là bạn không thể truy cập các biến cục bộ trong chúng. Bạn cần phải vượt qua và lưu trữ chúng một cách rõ ràng thông qua một hàm tạo.
Konrad Rudolph

1
@Konrad: Một vấn đề khác với họ là trong C ++ 98, bạn không được sử dụng các kiểu cục bộ làm tham số mẫu. Tôi nghĩ rằng C ++ 1x đã gỡ bỏ hạn chế đó, mặc dù. (Hay đó là C ++ 03?)
sbi

3
@luis: Tôi phải đồng ý với Fred. Bạn đang gắn một ý nghĩa với lambdas mà đơn giản là chúng không có (không có trong C ++ cũng như các ngôn ngữ khác mà tôi đã làm việc - không bao gồm Python và Ada, cho bản ghi). Hơn nữa, làm cho sự khác biệt đó chỉ là không có ý nghĩa trong C ++ vì C ++ không có chức năng cục bộ, thời gian. Nó chỉ có lambdas. Nếu bạn muốn giới hạn phạm vi của một thứ giống như hàm đối với một hàm, lựa chọn duy nhất của bạn là lambdas hoặc cấu trúc cục bộ được đề cập trong các câu trả lời khác. Tôi muốn nói rằng cái sau này quá phức tạp để được quan tâm thực tế.
Konrad Rudolph

2
@AustinWBryan Không, lambdas trong C ++ chỉ là đường cú pháp cho functor và không có phí. Có một câu hỏi với nhiều chi tiết hơn ở đâu đó trên trang web này.
Konrad Rudolph

42

Các lớp cục bộ đã được đề cập, nhưng đây là một cách để cho phép chúng xuất hiện nhiều hơn dưới dạng các hàm cục bộ, sử dụng một toán tử () quá tải và một lớp ẩn danh:

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

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

Tôi không khuyên bạn nên sử dụng nó, đây chỉ là một mẹo vui (có thể làm được, nhưng tôi không nên).


Cập nhật 2014:

Với sự phát triển của C ++ 11 một thời gian trước, giờ đây bạn có thể có các hàm cục bộ có cú pháp hơi gợi nhớ về JavaScript:

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

1
Nên operator () (unsigned int val), thiếu một bộ dấu ngoặc đơn.
Joe D

1
Trên thực tế, đây là một điều hoàn toàn hợp lý để làm nếu bạn cần truyền functor này cho một hàm stl hoặc thuật toán, như std::sort(), hoặc std::for_each().
Dima

1
@Dima: Thật không may, trong C ++ 03, các loại được xác định cục bộ không thể được sử dụng làm đối số mẫu. C ++ 0x sửa lỗi này, nhưng cũng cung cấp các giải pháp lambdas đẹp hơn nhiều, vì vậy bạn vẫn không làm điều đó.
Ben Voigt

Rất tiếc, bạn đã đúng. Lỗi của tôi. Tuy nhiên, đây không chỉ là một mẹo vui. Nó sẽ là một điều hữu ích nếu nó được cho phép. :)
Dima

3
Đệ quy được hỗ trợ. Tuy nhiên, bạn không thể sử dụng autođể khai báo biến. Stroustrup đưa ra ví dụ: function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };để đảo ngược một chuỗi con trỏ bắt đầu và kết thúc.
Eponymous

17

Không.

Bạn đang cố làm gì vậy?

cách giải quyết:

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

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

}

2
Lưu ý rằng cách tiếp cận khởi tạo lớp đi kèm với cấp phát bộ nhớ và do đó bị chi phối bởi cách tiếp cận tĩnh.
ManuelSchneid3r

14

Bắt đầu với C ++ 11, bạn có thể sử dụng lambdas thích hợp . Xem các câu trả lời khác để biết thêm chi tiết.


Câu trả lời cũ: Bạn có thể, sắp xếp, nhưng bạn phải gian lận và sử dụng một lớp giả:

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

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

Không chắc chắn bạn có thể, ngoại trừ bằng cách tạo một đối tượng thay thế (điều này làm tăng thêm tiếng ồn, IMO). Trừ khi có một số điều thông minh bạn có thể làm với các không gian tên, nhưng tôi không thể nghĩ về nó và có lẽ không nên lạm dụng ngôn ngữ nhiều hơn những gì chúng ta đã có. :)
Leo Davidson

The get-Rid-of-dummy :: nằm trong một trong những câu trả lời khác.
Sebastian Mach

8

Như những người khác đã đề cập, bạn có thể sử dụng các hàm lồng nhau bằng cách sử dụng các phần mở rộng ngôn ngữ gnu trong gcc. Nếu bạn (hoặc dự án của bạn) dính vào chuỗi công cụ gcc, mã của bạn sẽ chủ yếu di động trên các kiến ​​trúc khác nhau được nhắm mục tiêu bởi trình biên dịch gcc.

Tuy nhiên, nếu có một yêu cầu khả thi là bạn có thể cần phải biên dịch mã với một chuỗi công cụ khác, thì tôi sẽ tránh xa các tiện ích mở rộng đó.


Tôi cũng cẩn thận khi sử dụng các hàm lồng nhau. Chúng là một giải pháp tuyệt vời để quản lý cấu trúc của các khối mã phức tạp nhưng gắn kết (các phần không dành cho sử dụng bên ngoài / chung.) Chúng cũng rất hữu ích trong việc kiểm soát ô nhiễm không gian tên (một mối quan tâm rất thực với phức tạp tự nhiên / các lớp học dài trong các ngôn ngữ dài dòng.)

Nhưng giống như bất cứ điều gì, họ có thể được mở để lạm dụng.

Thật đáng buồn khi C / C ++ không hỗ trợ các tính năng như một tiêu chuẩn. Hầu hết các biến thể pascal và Ada đều làm (hầu hết tất cả các ngôn ngữ dựa trên Algol đều có). Tương tự với JavaScript. Tương tự với các ngôn ngữ hiện đại như Scala. Tương tự với các ngôn ngữ đáng kính như Erlang, Lisp hoặc Python.

Và cũng giống như với C / C ++, thật không may, Java (mà tôi kiếm được phần lớn cuộc sống của mình) thì không.

Tôi đề cập đến Java ở đây vì tôi thấy một số áp phích gợi ý sử dụng các lớp và phương thức của lớp như là các lựa chọn thay thế cho các hàm lồng nhau. Và đó cũng là cách giải quyết điển hình trong Java.

Câu trả lời ngắn gọn: Không.

Làm như vậy có xu hướng giới thiệu sự phức tạp giả tạo, không cần thiết trên một hệ thống phân cấp lớp. Với tất cả mọi thứ đều bằng nhau, lý tưởng là có một hệ thống phân cấp lớp (và các không gian tên và phạm vi bao gồm của nó) đại diện cho một miền thực tế càng đơn giản càng tốt.

Các hàm lồng nhau giúp đối phó với "riêng tư", độ phức tạp trong hàm. Thiếu các cơ sở đó, người ta nên cố gắng tránh truyền bá sự phức tạp "riêng tư" đó ra và vào mô hình lớp học của một người.

Trong phần mềm (và trong bất kỳ ngành kỹ thuật nào), mô hình hóa là vấn đề đánh đổi. Do đó, trong cuộc sống thực, sẽ có những ngoại lệ hợp lý cho những quy tắc đó (hay đúng hơn là hướng dẫn). Tiến hành cẩn thận, mặc dù.


8

Bạn không thể có các chức năng cục bộ trong C ++. Tuy nhiên, C ++ 11 có lambdas . Lambdas về cơ bản là các biến hoạt động như các hàm.

Một lambda có loại std::function( thực tế điều đó không hoàn toàn đúng , nhưng trong hầu hết các trường hợp, bạn có thể cho rằng nó là). Để sử dụng loại này, bạn cần phải #include <functional>. std::functionlà một mẫu, lấy làm đối số mẫu kiểu trả về và kiểu đối số, với cú pháp std::function<ReturnType(ArgumentTypes). Ví dụ, std::function<int(std::string, float)>là một lambda trả về một intvà lấy hai đối số, một std::stringvà một float. Một trong những phổ biến nhất là std::function<void()>, trả về không có gì và không có đối số.

Khi lambda được khai báo, nó được gọi giống như một hàm bình thường, sử dụng cú pháp lambda(arguments).

Để xác định lambda, hãy sử dụng cú pháp [captures](arguments){code}(có nhiều cách khác để làm điều đó, nhưng tôi sẽ không đề cập đến chúng ở đây). argumentslà những gì đối số lambda đưa ra, và codelà mã nên được chạy khi lambda được gọi. Thông thường bạn đặt [=]hoặc [&]như chụp. [=]có nghĩa là bạn nắm bắt tất cả các biến trong phạm vi mà giá trị được xác định theo giá trị, có nghĩa là chúng sẽ giữ giá trị mà chúng có khi lambda được khai báo. [&]có nghĩa là bạn nắm bắt tất cả các biến trong phạm vi bằng tham chiếu, có nghĩa là chúng sẽ luôn có giá trị hiện tại của chúng, nhưng nếu chúng bị xóa khỏi bộ nhớ thì chương trình sẽ bị sập. Dưới đây là một số ví dụ:

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

Bạn cũng có thể nắm bắt các biến cụ thể bằng cách chỉ định tên của chúng. Chỉ cần xác định tên của họ sẽ nắm bắt chúng theo giá trị, chỉ định tên của họ &trước đó sẽ nắm bắt chúng bằng cách tham chiếu. Ví dụ: [=, &foo]sẽ nắm bắt tất cả các biến theo giá trị ngoại trừ foosẽ được bắt bởi tham chiếu và [&, foo]sẽ nắm bắt tất cả các biến bằng tham chiếu ngoại trừ foosẽ được bắt theo giá trị. Bạn cũng có thể chỉ chụp các biến cụ thể, ví dụ [&foo]sẽ chụp footheo tham chiếu và sẽ không bắt được các biến khác. Bạn cũng có thể nắm bắt không có biến nào bằng cách sử dụng []. Nếu bạn cố gắng sử dụng một biến trong lambda mà bạn không nắm bắt được, nó sẽ không biên dịch. Đây là một ví dụ:

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

Bạn không thể thay đổi giá trị của một biến được bắt bởi giá trị bên trong lambda (các biến được bắt bởi giá trị có một constloại bên trong lambda). Để làm như vậy, bạn cần phải nắm bắt các biến bằng cách tham khảo. Đây là một exampmle:

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

Ngoài ra, gọi lambdas chưa được khởi tạo là hành vi không xác định và thường sẽ khiến chương trình bị sập. Ví dụ: không bao giờ làm điều này:

std::function<void()> lambda;
lambda();    //Undefined behavior because lambda is uninitialized

Ví dụ

Đây là mã cho những gì bạn muốn làm trong câu hỏi của bạn bằng lambdas:

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

Đây là một ví dụ nâng cao hơn về 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;
}

7

Không, nó không được phép. Cả C và C ++ đều không hỗ trợ tính năng này theo mặc định, tuy nhiên TonyK chỉ ra (trong các bình luận) rằng có các phần mở rộng cho trình biên dịch GNU C cho phép hành vi này trong C.


2
Nó được hỗ trợ bởi trình biên dịch GNU C, như một phần mở rộng đặc biệt. Nhưng chỉ dành cho C, không phải C ++.
TonyK

Ah. Tôi không có bất kỳ tiện ích mở rộng đặc biệt nào trong trình biên dịch C của mình. Đó là điều tốt để biết, mặc dù. Tôi sẽ thêm tiêu chuẩn đó vào câu trả lời của tôi.
Thomas Owens

Tôi đã sử dụng phần mở rộng gcc để hỗ trợ các hàm lồng nhau (trong C, tuy nhiên, không phải C ++). Các hàm lồng nhau là một điều tiện lợi (như trong Pascal và Ada) để quản lý các cấu trúc phức tạp nhưng gắn kết không có nghĩa là sử dụng chung. Miễn là một người sử dụng gcc toolchain, nó được đảm bảo là hầu hết có thể di động cho tất cả các kiến ​​trúc được nhắm mục tiêu. Nhưng nếu có sự thay đổi về việc phải biên dịch mã kết quả với trình biên dịch không phải là gcc, thì tốt nhất là tránh các phần mở rộng đó và bám sát nhất có thể với câu thần chú ansi / posix.
luis.espinal

7

Tất cả các thủ thuật này chỉ xem (ít nhiều) là các hàm cục bộ, nhưng chúng không hoạt động như vậy. Trong một hàm cục bộ, bạn có thể sử dụng các biến cục bộ của các hàm siêu của nó. Đó là loại bán toàn cầu. Không phải những thủ thuật này có thể làm điều đó. Gần nhất là thủ thuật lambda từ c ++ 0x, nhưng đóng của nó bị ràng buộc trong thời gian định nghĩa, không phải thời gian sử dụng.


Bây giờ tôi nghĩ rằng đây là câu trả lời tốt nhất. Mặc dù có thể khai báo một hàm trong một hàm (mà tôi sử dụng mọi lúc), nó không phải là một hàm cục bộ như được định nghĩa trong nhiều ngôn ngữ khác. Nó vẫn còn tốt để biết về khả năng.
Alexis Wilke

6

Bạn không thể định nghĩa một hàm miễn phí bên trong hàm khác trong C ++.


1
Không phải với ansi / posix, nhưng bạn có thể với các phần mở rộng gnu.
luis.espinal

4

Hãy để tôi đăng một giải pháp ở đây cho C ++ 03 mà tôi cho là sạch nhất có thể. *

#define DECLARE_LAMBDA(NAME, RETURN_TYPE, FUNCTION) \
    struct { RETURN_TYPE operator () FUNCTION } NAME;

...

int main(){
  DECLARE_LAMBDA(demoLambda, void, (){ cout<<"I'm a lambda!"<<endl; });
  demoLambda();

  DECLARE_LAMBDA(plus, int, (int i, int j){
    return i+j;
  });
  cout << "plus(1,2)=" << plus(1,2) << endl;
  return 0;
}

(*) trong thế giới C ++ sử dụng macro không bao giờ được coi là sạch.


Alexis, bạn có quyền nói rằng nó không hoàn toàn sạch sẽ. Nó vẫn gần như sạch sẽ vì nó thể hiện tốt những gì lập trình viên muốn làm, không có tác dụng phụ. Tôi coi nghệ thuật lập trình là viết biểu cảm dễ đọc của con người mà đọc giống như một cuốn tiểu thuyết.
Barney

2

Nhưng chúng ta có thể khai báo một hàm bên trong hàm main ():

int main()
{
    void a();
}

Mặc dù cú pháp là chính xác, đôi khi nó có thể dẫn đến "phân tích cú pháp nhất":

#include <iostream>


struct U
{
    U() : val(0) {}
    U(int val) : val(val) {}

    int val;
};

struct V
{
    V(U a, U b)
    {
        std::cout << "V(" << a.val << ", " << b.val << ");\n";
    }
    ~V()
    {
        std::cout << "~V();\n";
    }
};

int main()
{
    int five = 5;
    V v(U(five), U());
}

=> không có đầu ra chương trình.

(Chỉ cảnh báo Clang sau khi biên dịch).

Phân tích cú pháp khó chịu nhất của C ++ một lần nữa

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.