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;
}
}
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);
x_offset = 0;
y_offset = sizeof(int);
stack_pointer[x_offset] = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];
return_location = stack_pointer[y_offset];
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ì bar
bạn chỉ cần tra cứu ngăn xếp tại con trỏ ngăn xếp trước đó để truy cập foo
cá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, bar
có thể là đệ quy, trong trường hợp đó, một lệnh gọi cho trướcbar
có 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.
one
là định nghĩa hàm , hai là khai báo .