Tại sao tôi không thể xác định một hàm bên trong một hàm khác?


80

Đây không phải là câu hỏi về hàm lambda, tôi biết rằng tôi có thể gán lambda cho một biến.

Có ích gì khi cho phép chúng ta khai báo nhưng không xác định một hàm bên trong mã?

Ví dụ:

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

Vì vậy, những gì tôi muốn biết là tại sao C ++ lại cho phép twocái có vẻ vô dụng và threecái có vẻ phức tạp hơn nhiều nhưng lại không cho phép one?

BIÊN TẬP:

Từ các câu trả lời, có vẻ như khai báo trong mã có thể ngăn ngừa ô nhiễm không gian tên, điều mà tôi hy vọng được nghe là tại sao khả năng khai báo hàm được cho phép nhưng khả năng xác định hàm lại không được phép.


3
Đầu tiên oneđịnh nghĩa hàm , hai là khai báo .
Một số lập trình viên dude

9
Tôi nghĩ rằng bạn đã hiểu sai các thuật ngữ - bạn muốn hỏi "Có ích gì khi cho phép chúng tôi khai báo, nhưng không xác định một hàm bên trong mã?". Và trong khi chúng tôi đang ở đó, bạn có thể có nghĩa là "bên trong một hàm ". Tất cả đều là "mã".
Peter - Phục hồi Monica

14
Nếu bạn đang hỏi tại sao ngôn ngữ lại có những điểm kỳ quặc và không nhất quán: bởi vì nó đã phát triển trong vài thập kỷ, thông qua công việc của nhiều người với nhiều ý tưởng khác nhau, từ những ngôn ngữ được phát minh ra vì những lý do khác nhau vào những thời điểm khác nhau. Nếu bạn đang hỏi tại sao nó lại có điều kỳ lạ này: bởi vì không ai (cho đến nay) nghĩ rằng các định nghĩa hàm cục bộ đủ hữu ích để chuẩn hóa.
Mike Seymour

4
@MikeSeymour đã làm đúng như vậy. C không có cấu trúc tốt như Pascal, và luôn chỉ cho phép các định nghĩa hàm cấp cao nhất. Vì vậy, lý do là lịch sử, cộng với nhu cầu thiếu sót để thay đổi nó. Việc khai báo hàm là có thể chỉ là hệ quả của việc khai báo theo phạm vi nói chung là có thể. Cấm điều đó cho các chức năng sẽ có nghĩa là một quy tắc bổ sung.
Peter - Phục hồi Monica

3
@JonathanMee: Có thể là vì nói chung, khai báo được cho phép trong các khối và không có lý do cụ thể nào để cấm khai báo hàm; đơn giản hơn là chỉ cho phép bất kỳ khai báo nào mà không có trường hợp đặc biệt. Nhưng "tại sao" không thực sự là một câu hỏi có thể trả lời được; ngôn ngữ là cái mà nó là vì đó là cách nó phát triển.
Mike Seymour

Câu trả lời:


41

Không rõ tại sao lại onekhông được phép; các hàm lồng nhau đã được đề xuất cách đây rất lâu trong N0295 có nội dung:

Chúng ta thảo luận về việc giới thiệu các hàm lồng nhau vào C ++. Các hàm lồng nhau đã được hiểu rõ và việc giới thiệu chúng đòi hỏi ít nỗ lực từ các nhà cung cấp trình biên dịch, lập trình viên hoặc ủy ban. Các hàm lồng nhau mang lại những lợi thế đáng kể, […]

Rõ ràng là đề xuất này đã bị từ chối, nhưng vì chúng tôi không có biên bản cuộc họp trực tuyến nên 1993chúng tôi không có nguồn khả thi về lý do cho việc từ chối này.

Trên thực tế, đề xuất này được ghi nhận trong các biểu thức Lambda và các bao đóng cho C ++ như một giải pháp thay thế khả thi:

Một bài báo [Bre88] và đề xuất N0295 cho ủy ban C ++ [SH93] đề xuất thêm các hàm lồng nhau vào C ++. Các hàm lồng nhau tương tự như biểu thức lambda, nhưng được định nghĩa là các câu lệnh trong thân hàm và không thể sử dụng bao đóng kết quả trừ khi hàm đó đang hoạt động. Các đề xuất này cũng không bao gồm việc thêm một kiểu mới cho mỗi biểu thức lambda, mà thay vào đó, triển khai chúng giống như các hàm bình thường hơn, bao gồm việc cho phép một loại con trỏ hàm đặc biệt tham chiếu đến chúng. Cả hai đề xuất này đều có trước việc bổ sung các mẫu vào C ++ và do đó không đề cập đến việc sử dụng các hàm lồng nhau kết hợp với các thuật toán chung. Ngoài ra, các đề xuất này không có cách nào để sao chép các biến cục bộ vào một bao đóng và do đó, các hàm lồng nhau mà chúng tạo ra hoàn toàn không sử dụng được bên ngoài hàm bao của chúng

Xem xét hiện tại chúng ta có lambdas, chúng ta khó có thể thấy các hàm lồng nhau vì như bài báo đã phác thảo, chúng là các lựa chọn thay thế cho cùng một vấn đề và các hàm lồng nhau có một số hạn chế so với lambdas.

Đối với phần này của câu hỏi của bạn:

// This is legal, but why would I want this?
int two(int bar);

Có những trường hợp đây sẽ là một cách hữu ích để gọi hàm bạn muốn. Phần dự thảo tiêu chuẩn C ++ 3.4.1 [basic.lookup.unqual] cung cấp cho chúng ta một ví dụ thú vị:

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
Câu hỏi trong ví dụ 3.4.1 mà bạn đưa ra: Không thể người gọi trong main chỉ cần viết ::g(parm, 1)để gọi hàm trong không gian tên chung? Hoặc gọi g(parm, 1.0f);cái nào sẽ dẫn đến kết quả phù hợp hơn với mong muốn g?
Peter - Phục hồi Monica

@PeterSchneider Tôi đã tuyên bố quá mạnh ở đó, tôi đã điều chỉnh nó.
Shafik Yaghmour,

1
Tôi muốn thêm nhận xét ở đây: Câu trả lời này không được chấp nhận bởi vì nó đã làm công việc tốt nhất là giải thích tại sao trong khai báo hàm mã được cho phép; nhưng vì nó đã làm tốt nhất công việc mô tả lý do tại sao trong các định nghĩa hàm mã không được phép, đó là câu hỏi thực tế. Và cụ thể nó phác thảo cụ thể lý do tại sao việc triển khai giả định trong các hàm mã sẽ khác với việc triển khai lambdas. +1
Jonathan Mee

1
@JonathanMee: Làm thế nào trên thế giới này: "... chúng tôi không có nguồn có thể có lý do cho sự từ chối này." đủ điều kiện là công việc tốt nhất để mô tả lý do tại sao các định nghĩa hàm lồng nhau không được phép (hoặc thậm chí cố gắng mô tả nó?)
Jerry Coffin

@JerryCoffin Câu trả lời bao gồm lý do chính thức tại sao lambdas đã là một tập hợp siêu định nghĩa hàm mã khiến việc triển khai của chúng không cần thiết: "Không thể sử dụng đóng kết quả trừ khi hàm đó đang hoạt động ... Ngoài ra, những đề xuất này không có cách nào để sao chép biến cục bộ thành một bao đóng. " Tôi giả định rằng bạn đang hỏi tại sao phân tích của bạn về độ phức tạp bổ sung được đặt trên các trình biên dịch không phải là câu trả lời mà tôi chấp nhận. Nếu vậy: Bạn nói về khó khăn của điều gì đó lambdas đã hoàn thành, trong các định nghĩa mã rõ ràng có thể được triển khai chính xác như lambdas.
Jonathan Mee

31

Vâng, câu trả lời là "lý do lịch sử". Trong C, bạn có thể có các khai báo hàm ở phạm vi khối và các nhà thiết kế C ++ không thấy lợi ích khi loại bỏ tùy chọn đó.

Một ví dụ sử dụng sẽ là:

#include <iostream>

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

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

IMO đây là một ý tưởng tồi vì rất dễ mắc lỗi khi cung cấp một khai báo không khớp với định nghĩa thực của hàm, dẫn đến hành vi không xác định sẽ không được trình biên dịch chẩn đoán.


10
"Điều này thường được coi là một ý tưởng tồi' - trích dẫn yêu cầu.
Richard Hodges

4
@RichardHodges: Chà, các khai báo hàm thuộc về tệp tiêu đề và việc triển khai trong tệp .c hoặc .cpp, vì vậy việc có các khai báo này bên trong định nghĩa hàm vi phạm một trong hai nguyên tắc đó.
MSalters,

2
Làm cách nào để ngăn việc khai báo khác với định nghĩa?
Richard Hodges

1
@JonathanMee: Tôi đang nói rằng, nếu khai báo bạn đang sử dụng không khả dụng khi hàm được xác định, trình biên dịch có thể không kiểm tra xem khai báo có khớp với định nghĩa hay không. Vì vậy, bạn có thể có một khai báo cục bộ some_type f();và một định nghĩa trong một đơn vị dịch khác another_type f() {...}. Trình biên dịch không thể cho bạn biết rằng những điều này không khớp và việc gọi fvới khai báo sai sẽ đưa ra hành vi không xác định. Vì vậy, bạn nên chỉ có một khai báo, trong tiêu đề và đưa tiêu đề đó vào nơi hàm được xác định, cũng như nơi nó được sử dụng.
Mike Seymour,

6
Tôi nghĩ những gì bạn đang nói là thực tế phổ biến là đặt các khai báo hàm trong tệp tiêu đề thường hữu ích. Tôi không nghĩ có ai sẽ không đồng ý với điều đó. Điều tôi thấy không có lý do là khẳng định rằng việc khai báo một hàm bên ngoài ở phạm vi hàm là 'thường được coi là một ý tưởng tồi'.
Richard Hodges

23

Trong ví dụ bạn đưa ra, void two(int)đang được khai báo là một hàm bên ngoài, với khai báo đó chỉ có giá trị trong phạm vi của mainhàm .

Điều đó hợp lý nếu bạn chỉ muốn twocung cấp tên bên trong main()để tránh làm ô nhiễm không gian tên chung trong đơn vị biên dịch hiện tại.

Ví dụ để trả lời các nhận xét:

main.cpp:

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

foo.cpp:

int foo() {
  return 0;
}

không cần tệp tiêu đề. biên dịch và liên kết với

c++ main.cpp foo.cpp 

nó sẽ biên dịch và chạy, và chương trình sẽ trả về 0 như mong đợi.


Cũng sẽ không twophải được xác định trong tệp do đó gây ra ô nhiễm dù sao?
Jonathan Mee

1
@JonathanMee không, two()có thể được định nghĩa trong một đơn vị biên dịch hoàn toàn khác.
Richard Hodges

Tôi cần giúp đỡ để hiểu cách thức hoạt động. Bạn sẽ không phải bao gồm tiêu đề mà nó đã được khai báo? Tại thời điểm đó nó sẽ được khai báo, phải không? Tôi chỉ không thấy làm thế nào bạn có thể xác định nó trong mã và bằng cách nào đó không bao gồm tệp khai báo nó?
Jonathan Mee

5
@JonathanMee Không có gì đặc biệt về tiêu đề. Chúng chỉ là một nơi thuận tiện để kê khai. Một khai báo trong một hàm cũng hợp lệ như một khai báo trong tiêu đề. Vì vậy, không, bạn sẽ không cần phải bao gồm tiêu đề của những gì bạn đang liên kết đến (thậm chí có thể không có tiêu đề nào cả).
Khối

1
@JonathanMee Trong ngôn ngữ C / C ++, định nghĩa và cách triển khai giống nhau. Bạn có thể khai báo một hàm thường xuyên như bạn muốn, nhưng bạn chỉ có thể định nghĩa nó một lần. Phần khai báo không cần phải có trong tệp kết thúc bằng .h - bạn có thể có tệp use.cpp có thanh chức năng gọi foo (khai báo foo trong nội dung của nó) và tệp cung cấp.cpp định nghĩa foo, và nó sẽ hoạt động tốt miễn là bạn không làm sai bước liên kết.
Khối

19

Bạn có thể làm những điều này, phần lớn là vì chúng thực sự không khó thực hiện.

Từ quan điểm của trình biên dịch, có một khai báo hàm bên trong một hàm khác là khá đơn giản để thực hiện. Trình biên dịch cần một cơ chế để cho phép các khai báo bên trong hàm xử lý các khai báo khác (ví dụ int x;:) bên trong một hàm.

Nó thường sẽ có một cơ chế chung để phân tích cú pháp một khai báo. Đối với người viết trình biên dịch, không thực sự quan trọng cho dù cơ chế đó được gọi khi phân tích mã bên trong hay bên ngoài của một hàm khác - nó chỉ là một khai báo, vì vậy khi nó đủ để biết rằng có một khai báo, nó gọi một phần của trình biên dịch xử lý các khai báo.

Trên thực tế, việc cấm các khai báo cụ thể này bên trong một hàm có thể sẽ thêm phức tạp, bởi vì trình biên dịch sau đó sẽ cần kiểm tra hoàn toàn vô cớ để xem liệu nó đã xem mã bên trong định nghĩa hàm chưa và dựa trên đó quyết định xem nên cho phép hay cấm điều này cụ thể tờ khai.

Điều đó đặt ra câu hỏi về việc một hàm lồng nhau khác nhau như thế nào. Một hàm lồng nhau là khác nhau vì nó ảnh hưởng như thế nào đến việc tạo mã. Trong các ngôn ngữ cho phép các hàm lồng nhau (ví dụ: Pascal), bạn thường mong đợi rằng mã trong hàm lồng nhau có quyền truy cập trực tiếp vào các biến của hàm mà nó được lồng trong đó. Ví dụ:

int foo() { 
    int x;

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

Không có hàm cục bộ, mã để truy cập các biến cục bộ khá đơn giản. Trong một triển khai điển hình, khi thực thi đi vào hàm, một số khối không gian cho các biến cục bộ được cấp phát trên ngăn xếp. Tất cả các biến cục bộ được cấp phát trong một khối duy nhất đó và mỗi biến được coi như một phần bù trừ từ đầu (hoặc cuối) của khối. Ví dụ, hãy xem xét một hàm giống như sau:

int f() { 
   int x;
   int y;
   x = 1;
   y = x;
   return y;
}

Một trình biên dịch (giả sử nó không tối ưu hóa mã bổ sung) có thể tạo mã cho điều này gần tương đương với điều nà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);

Đặc biệt, nó có một vị trí trỏ đến phần đầu của khối các biến cục bộ và tất cả quyền truy cập vào các biến cục bộ là các phần bù từ vị trí đó.

Với các hàm lồng nhau, điều đó không còn xảy ra nữa - thay vào đó, một hàm có quyền truy cập không chỉ vào các biến cục bộ của chính nó, mà còn cho các biến cục bộ của tất cả các hàm mà nó được lồng vào nhau. Thay vì chỉ có một "stack_pointer" mà từ đó nó tính toán độ lệch, nó cần phải đi ngược lại ngăn xếp để tìm stack_pointers cục bộ cho các hàm mà nó được lồng vào nhau.

Bây giờ, trong một trường hợp nhỏ, điều đó cũng không khủng khiếp như vậy - nếu barđược lồng vào bên trong foo, thì barbạn chỉ cần tra cứu ngăn xếp tại con trỏ ngăn xếp trước đó để truy cập foocác biến của. Đúng?

Sai lầm! Vâng, có những trường hợp điều này có thể đúng, nhưng nó không nhất thiết phải như vậy. Đặc biệt, barcó thể là đệ quy, trong trường hợp đó, một lệnh gọi cho trướcbarcó thể phải xem một số mức gần như tùy ý sao lưu ngăn xếp để tìm các biến của hàm xung quanh. Nói chung, bạn cần thực hiện một trong hai việc: hoặc bạn đặt một số dữ liệu bổ sung vào ngăn xếp, để nó có thể tìm kiếm sao lưu ngăn xếp tại thời điểm chạy để tìm khung ngăn xếp của chức năng xung quanh của nó, hoặc nếu không, bạn chuyển một con trỏ đến khung ngăn xếp của hàm xung quanh như một tham số ẩn đối với hàm lồng nhau. Ồ, nhưng cũng không nhất thiết chỉ có một hàm xung quanh - nếu bạn có thể lồng các hàm, bạn có thể lồng chúng (nhiều hơn hoặc ít hơn) sâu tùy ý, vì vậy bạn cần phải sẵn sàng truyền một số tham số ẩn tùy ý. Điều đó có nghĩa là bạn thường kết thúc với một cái gì đó giống như một danh sách liên kết các khung ngăn xếp với các chức năng xung quanh,

Tuy nhiên, điều đó có nghĩa là việc truy cập vào một biến "cục bộ" có thể không phải là một vấn đề tầm thường. Việc tìm đúng khung ngăn xếp để truy cập biến có thể không phải là chuyện nhỏ, vì vậy việc truy cập vào các biến của các hàm xung quanh cũng (ít nhất là thường) chậm hơn so với truy cập vào các biến cục bộ thực sự. Và tất nhiên, trình biên dịch phải tạo mã để tìm các khung ngăn xếp phù hợp, truy cập các biến thông qua bất kỳ số lượng khung ngăn xếp tùy ý nào, v.v.

Đây là sự phức tạp mà C đã tránh bằng cách cấm các hàm lồng nhau. Bây giờ, chắc chắn đúng rằng một trình biên dịch C ++ hiện tại là một loại quái thú khá khác với trình biên dịch C cổ điển của năm 1970. Với những thứ như đa kế thừa, thừa kế ảo, trình biên dịch C ++ phải xử lý những thứ về bản chất chung này trong mọi trường hợp (ví dụ: việc tìm vị trí của một biến lớp cơ sở trong những trường hợp như vậy cũng có thể không nhỏ). Trên cơ sở tỷ lệ phần trăm, việc hỗ trợ các hàm lồng nhau sẽ không thêm nhiều phức tạp cho trình biên dịch C ++ hiện tại (và một số, chẳng hạn như gcc, đã hỗ trợ chúng).

Đồng thời, nó cũng hiếm khi bổ sung nhiều tiện ích. Đặc biệt, nếu bạn muốn định nghĩa một cái gì đó hoạt động giống như một hàm bên trong một hàm, bạn có thể sử dụng biểu thức lambda. Điều này thực sự tạo ra là một đối tượng (ví dụ, một thể hiện của một số lớp) làm quá tải toán tử gọi hàm ( operator()) nhưng nó vẫn cung cấp các khả năng giống như hàm. Mặc dù vậy, nó làm cho việc thu thập (hoặc không) dữ liệu từ bối cảnh xung quanh rõ ràng hơn, cho phép nó sử dụng các cơ chế hiện có hơn là phát minh ra một cơ chế và bộ quy tắc hoàn toàn mới để sử dụng nó.

Điểm mấu chốt: mặc dù ban đầu có vẻ như các khai báo lồng nhau là khó và các hàm lồng nhau là tầm thường, nhưng ít nhiều điều ngược lại là đúng: các hàm lồng nhau thực sự phức tạp hơn nhiều để hỗ trợ so với các khai báo lồng nhau.


5

Cái đầu tiên là định nghĩa hàm và nó không được phép. Rõ ràng, wt là cách sử dụng đặt định nghĩa của một hàm bên trong một hàm khác.

Nhưng hai phần khác chỉ là tuyên bố. Hãy tưởng tượng bạn cần sử dụng int two(int bar);hàm bên trong phương thức chính. Nhưng nó được định nghĩa bên dưới main()hàm, do đó khai báo hàm bên trong hàm khiến bạn sử dụng hàm đó với các khai báo.

Điều tương tự cũng áp dụng cho phần thứ ba. Khai báo lớp bên trong hàm cho phép bạn sử dụng một lớp bên trong hàm mà không cần cung cấp tiêu đề hoặc tham chiếu thích hợp.

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
"Giảm tốc" là gì? Ý bạn là "tuyên bố"?
Peter Mortensen

4

Tính năng ngôn ngữ này được kế thừa từ C, nơi nó phục vụ một số mục đích trong những ngày đầu của C (có thể khai báo phạm vi chức năng?) . Tôi không biết liệu tính năng này có được các lập trình viên C hiện đại sử dụng nhiều hay không và tôi thực sự nghi ngờ về nó.

Vì vậy, để tổng hợp câu trả lời:

không có mục đích cho tính năng này trong C ++ hiện đại (mà tôi biết, ít nhất là), nó ở đây vì khả năng tương thích ngược C ++-to-C (tôi cho là :)).


Cảm ơn bình luận bên dưới:

Nguyên mẫu hàm được xác định phạm vi cho hàm mà nó được khai báo, vì vậy người ta có thể có một không gian tên toàn cục gọn gàng hơn - bằng cách tham chiếu đến các hàm / ký hiệu bên ngoài mà không có #include.


mục đích là kiểm soát phạm vi của tên để tránh ô nhiễm không gian tên toàn cầu.
Richard Hodges

Được rồi, tôi cho rằng nó hữu ích cho các tình huống, khi bạn muốn tham chiếu đến các hàm / biểu tượng bên ngoài mà không làm ô nhiễm không gian tên chung với #include! Cảm ơn đã chỉ ra điều đó. Tôi sẽ chỉnh sửa.
mr.pd

4

Trên thực tế, có một trường hợp sử dụng có thể tưởng tượng được. Nếu bạn muốn đảm bảo rằng một hàm nhất định được gọi (và mã của bạn biên dịch), bất kể mã xung quanh khai báo gì, bạn có thể mở khối của riêng mình và khai báo nguyên mẫu hàm trong đó. (Nguồn cảm hứng ban đầu từ Johannes Schaub, https://stackoverflow.com/a/929902/3150802 , qua TeKa, https://stackoverflow.com/a/8821992/3150802 ).

Điều này có thể đặc biệt hữu ích nếu bạn phải bao gồm các tiêu đề mà bạn không kiểm soát hoặc nếu bạn có macro nhiều dòng có thể được sử dụng trong mã không xác định.

Điều quan trọng là một khai báo cục bộ thay thế các khai báo trước đó trong khối bao quanh trong cùng. Mặc dù điều đó có thể tạo ra các lỗi nhỏ (và tôi nghĩ là bị cấm trong C #), nhưng nó có thể được sử dụng một cách có ý thức. Xem xét:

// somebody's header
void f();

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

Việc liên kết có thể thú vị vì rất có thể các tiêu đề thuộc về một thư viện, nhưng tôi đoán bạn có thể điều chỉnh các đối số của trình liên kết để chúng f()được giải quyết cho hàm của bạn vào thời điểm thư viện đó được xem xét. Hoặc bạn bảo nó bỏ qua các ký hiệu trùng lặp. Hoặc bạn không liên kết với thư viện.


Vì vậy, hãy giúp tôi ở đây, nơi nào sẽ fđược xác định trong ví dụ của bạn? Tôi sẽ không gặp phải lỗi xác định lại hàm vì những lỗi này chỉ khác nhau theo kiểu trả về?
Jonathan Mee

@JonathanMee hmmm ... f () có thể được định nghĩa bằng một đơn vị dịch khác, tôi nghĩ. Nhưng có lẽ trình liên kết sẽ chùn bước nếu bạn cũng liên kết với thư viện giả định, tôi cho rằng bạn đúng. Vì vậy, bạn không thể làm điều đó ;-), hoặc ít nhất phải bỏ qua nhiều định nghĩa.
Peter - Phục hồi Monica

Ví dụ xấu. Không có sự phân biệt giữa void f()int f()trong C ++ vì giá trị trả về của một hàm không phải là một phần của chữ ký của hàm trong C ++. Thay đổi khai báo thứ hai thành int f(int)và tôi sẽ xóa phiếu phản đối của mình.
David Hammen

@DavidHammen Cố gắng biên dịch i = f();sau khi khai báo void f(). "Không phân biệt" chỉ là một nửa sự thật ;-). Tôi thực sự đã sử dụng "chữ ký" của hàm không quá tải vì nếu không thì toàn bộ trường hợp sẽ không cần thiết trong C ++ vì hai hàm với các kiểu / số tham số khác nhau có thể cùng tồn tại một cách vui vẻ.
Peter - Phục hồi Monica

@DavidHammen Thật vậy, sau khi đọc câu trả lời của Shafik, tôi tin rằng chúng ta có ba trường hợp: 1. Chữ ký khác nhau về thông số. Không có vấn đề trong C ++, quá tải đơn giản và các quy tắc phù hợp nhất hoạt động. 2. Chữ ký không khác nhau chút nào. Không có vấn đề ở cấp độ ngôn ngữ; chức năng được giải quyết bằng cách liên kết chống lại việc thực hiện mong muốn. 3. Sự khác biệt chỉ ở kiểu trả về. một vấn đề ở mức độ ngôn ngữ, như chứng minh; giải quyết quá tải không hoạt động; chúng ta phải khai báo một hàm với một chữ ký khác liên kết một cách thích hợp.
Peter - Phục hồi Monica

3

Đây không phải là câu trả lời cho câu hỏi OP, mà là câu trả lời cho một số nhận xét.

Tôi không đồng ý với những điểm này trong các nhận xét và câu trả lời: 1 rằng các khai báo lồng nhau được cho là vô hại và 2 rằng các định nghĩa lồng nhau là vô ích.

1 Ví dụ phản chứng chính cho tính vô hại được cho là vô hại của các khai báo hàm lồng nhau là Phân tích cú pháp nổi tiếng gây khó chịu nhất . IMO, sự lây lan của sự nhầm lẫn do nó gây ra là đủ để đảm bảo một quy tắc bổ sung cấm các khai báo lồng nhau.

2 Ví dụ đầu tiên đối với sự vô dụng bị cáo buộc của các định nghĩa hàm lồng nhau là thường xuyên phải thực hiện cùng một hoạt động ở một số vị trí bên trong chính xác một hàm. Có một giải pháp rõ ràng cho điều này:

private:
inline void bar(int abc)
{
    // Do the repeating operation
}

public: 
void foo()
{
    int a, b, c;
    bar(a);
    bar(b);
    bar(c);
}

Tuy nhiên, giải pháp này thường đủ làm ô nhiễm định nghĩa lớp với nhiều hàm riêng tư, mỗi hàm được sử dụng trong chính xác một trình gọi. Một khai báo hàm lồng nhau sẽ gọn gàng hơn nhiều.


1
Tôi nghĩ rằng điều này tạo nên một bản tóm tắt tốt về động cơ câu hỏi của tôi. Nếu bạn nhìn vào phiên bản gốc mà tôi đã trích dẫn MVP, nhưng tôi vẫn tiếp tục bị đánh giá cao trong các nhận xét (câu hỏi của riêng tôi) khi được thông báo rằng MVP không liên quan: (Tôi chỉ không thể tìm ra cách thức có thể gây hại trong khai báo mã vẫn còn ở đây , nhưng khả năng hữu ích trong các định nghĩa mã thì không. Tôi đã cho bạn +1 cho các ví dụ hữu ích.
Jonathan Mee

2

Trả lời cụ thể câu hỏi này:

Từ các câu trả lời, có vẻ như khai báo trong mã có thể ngăn ngừa ô nhiễm không gian tên, điều mà tôi hy vọng được nghe là tại sao khả năng khai báo hàm được cho phép nhưng khả năng xác định hàm lại không được phép.

Vì hãy xem xét mã này:

int main()
{
  int foo() {

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

Câu hỏi dành cho nhà thiết kế ngôn ngữ:

  1. Nên foo()có sẵn cho các chức năng khác?
  2. Nếu vậy, tên của nó phải là gì? int main(void)::foo()?
  3. (Lưu ý rằng 2 sẽ không thể thực hiện được trong C, người khởi tạo C ++)
  4. Nếu chúng ta muốn một hàm cục bộ, chúng ta đã có một cách - biến nó thành một thành viên tĩnh của một lớp được xác định cục bộ. Vì vậy, chúng ta nên thêm một phương thức cú pháp khác để đạt được kết quả tương tự? Tại sao làm vậy? Nó sẽ không làm tăng gánh nặng bảo trì của các nhà phát triển trình biên dịch C ++?
  5. Và như thế...

Rõ ràng hành vi này được xác định cho lambdas? Tại sao các hàm không được định nghĩa trong mã?
Jonathan Mee

Lambda chỉ đơn thuần là viết tắt để viết một đối tượng hàm. Trường hợp đặc biệt của lambda không nắm bắt đối số tương đương với định nghĩa hàm cục bộ, cũng như viết một đối tượng hàm không có thành viên dữ liệu.
Richard Hodges

Tôi chỉ chỉ ra rằng lambdas, trong các hàm được khai báo mã đã loại bỏ tất cả các điểm của bạn. Nó sẽ không được gia tăng "gánh nặng".
Jonathan Mee

@JonathanMee nếu bạn cảm thấy mạnh mẽ về nó, bằng mọi cách, hãy gửi RFC cho ủy ban tiêu chuẩn c ++.
Richard Hodges

Câu trả lời của Shafik Yaghmour đề cập đến điều đã được thực hiện. Cá nhân tôi muốn thấy việc loại bỏ khả năng khai báo các hàm trong mã nếu chúng không cho phép chúng tôi định nghĩa chúng. Câu trả lời của Richard Hodges đã giải thích rất tốt lý do tại sao chúng ta vẫn cần khả năng khai báo trong khai báo mã.
Jonathan Mee

1

Chỉ muốn chỉ ra rằng trình biên dịch GCC cho phép bạn khai báo các hàm bên trong các hàm. Đọc thêm về nó ở đây . Cũng với sự ra đời của lambdas vào C ++, câu hỏi này giờ đã lỗi thời một chút.


Khả năng khai báo tiêu đề hàm bên trong các hàm khác, tôi thấy hữu ích trong trường hợp sau:

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

Chúng ta có gì ở đây? Về cơ bản, bạn có một hàm được cho là được gọi từ main, vì vậy những gì bạn làm là bạn khai báo nó như bình thường. Nhưng rồi bạn nhận ra, chức năng này cũng cần một chức năng khác để giúp nó làm được những gì. Vì vậy, thay vì khai báo hàm helper đó ở trên main, bạn khai báo nó bên trong hàm cần nó và sau đó nó có thể được gọi từ hàm đó và chỉ hàm đó.

Quan điểm của tôi là, việc khai báo tiêu đề hàm bên trong các hàm có thể là một phương pháp đóng gói hàm gián tiếp, cho phép một hàm ẩn một số phần của những gì nó đang làm bằng cách ủy quyền cho một số hàm khác mà chỉ nó mới biết, gần như tạo ra ảo giác về một chức năng .


Tôi hiểu rằng chúng ta có thể xác định một lambda nội tuyến. Tôi hiểu rằng chúng ta có thể khai báo một hàm nội tuyến, nhưng đó là nguồn gốc của phân tích cú pháp khó chịu nhất , vì vậy câu hỏi của tôi là nếu tiêu chuẩn sẽ giữ lại chức năng chỉ phục vụ để gây ra cơn thịnh nộ trong lập trình viên, thì các lập trình viên sẽ không thể định nghĩa chức năng nội tuyến quá? Câu trả lời của Richard Hodges đã giúp tôi hiểu được nguồn gốc của vấn đề này.
Jonathan Mee

0

Có thể cho phép khai báo hàm lồng nhau đối với 1. Tham chiếu chuyển tiếp 2. Để có thể khai báo một con trỏ tới (các) hàm và chuyển xung quanh (các) hàm khác trong một phạm vi giới hạn.

Định nghĩa hàm lồng nhau không được cho phép có thể do các vấn đề như 1. Tối ưu hóa 2. Đệ quy (bao gồm và (các) hàm được xác định lồng nhau) 3. Re-entrancy 4. Đồng thời và các vấn đề truy cập đa luồng khác.

Từ sự hiểu biết hạn hẹp của tôi :)

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.