2013-07-22 39 views
24

Tôi không thể nghĩ ra một ngôn ngữ RAII thực sự cũng có tối ưu hóa cuộc gọi đuôi trong thông số kỹ thuật, nhưng tôi biết nhiều triển khai C++ có thể làm điều đó như một tối ưu hóa cụ thể cho việc triển khai thực hiện.Có thể tối ưu hóa cuộc gọi đuôi và RAII cùng tồn tại?

Điều này đặt ra một câu hỏi cho những triển khai thực hiện: cho rằng destructors được gọi ở cuối phạm vi biến tự động và không theo thói quen thu gom rác riêng biệt, không vi phạm ràng buộc của TCO. là lệnh cuối cùng ở cuối hàm?

Ví dụ: -

#include <iostream> 

class test_object { 
public: 
    test_object() { std::cout << "Constructing...\n"; } 
    ~test_object() { std::cout << "Destructing...\n"; } 
}; 

void test_function(int count); 

int main() 
{ 
    test_function(999); 
} 

void test_function(int count) 
{ 
    if (!count) return; 
    test_object obj; 
    test_function(count - 1); 
} 

"Xây dựng ..." sẽ được viết 999 lần và sau đó "huỷ ..." khác 999 lần. Cuối cùng, 999 test_object trường hợp sẽ được tự động cấp phát trước khi thư giãn. Nhưng giả sử một thực hiện đã có TCO, sẽ 1000 khung stack tồn tại hoặc chỉ 1?

Trình hủy sau khi cuộc gọi đệ quy có va chạm với các yêu cầu triển khai Tact defacto không?

+3

Thú vị mặc dù thực sự. Tôi sẽ nói "tất nhiên nó không ngăn chặn TCO", nhưng tôi càng nhìn vào nó, tôi càng nghĩ rằng nó ... –

+2

Không có cuộc gọi đuôi ở đây (và kiểm tra mã của bạn, nó không biên dịch như là). Mặc dù trong lý thuyết một trình biên dịch tốt có thể nhận thấy các mô hình và biến điều này thành 2 vòng. –

+5

Câu trả lời ngắn gọn: có, những người hủy quyết định ngăn chặn TCO. Câu trả lời dài: thực sự một số trình biên dịch (như LLVM tôi tin) thực hiện một hình thức dễ dãi hơn của TCO và có thể chịu đựng được nhiều trường hợp hơn ... –

Trả lời

13

Được thực hiện theo mệnh giá, có vẻ như RAII hoạt động với TCO. Tuy nhiên, hãy nhớ rằng có một số cách mà trình biên dịch có thể "thoát khỏi nó", vì vậy để nói. Trường hợp đầu tiên và rõ ràng nhất là nếu destructor là tầm thường, có nghĩa là nó là destructor mặc định (compiler-generated) và tất cả các đối tượng phụ có các destructors tầm thường, thì destructor có hiệu quả không tồn tại (luôn luôn tối ưu hóa). Trong trường hợp đó, TCO có thể được thực hiện như bình thường.

Sau đó, trình phá hủy có thể được gạch chân (mã của nó được lấy và đặt trực tiếp vào hàm như trái ngược với được gọi là hàm). Trong trường hợp đó, nó sẽ chỉ có một số mã "clean-up" sau câu lệnh return. Trình biên dịch được phép sắp xếp lại các hoạt động nếu nó có thể xác định rằng kết quả cuối cùng là giống nhau (quy tắc "as-if"), và nó sẽ làm như vậy (nói chung) nếu việc sắp xếp lại dẫn đến mã tốt hơn, và tôi cho rằng TCO là một trong những cân nhắc đang được hầu hết các trình biên dịch áp dụng (ví dụ, nếu nó có thể sắp xếp lại những thứ sao cho mã trở nên thích hợp cho TCO, thì nó sẽ làm điều đó).

Và đối với phần còn lại của các trường hợp, nơi trình biên dịch không thể "đủ thông minh" để tự thực hiện, thì nó trở thành trách nhiệm của người lập trình. Sự hiện diện của cuộc gọi destructor tự động này làm cho nó khó hơn một chút đối với lập trình viên để xem mã TCO-inhibiting clean-up sau cuộc gọi đuôi, nhưng nó không tạo ra bất kỳ sự khác biệt nào về khả năng của lập trình viên để làm cho chức năng một ứng cử viên cho TCO.Ví dụ:

void nonRAII_recursion(int a) { 
    int* arr = new int[a]; 
    // do some stuff with array "arr" 
    delete[] arr; 
    nonRAII_recursion(--a); // tail-call 
}; 

Bây giờ, một ngây thơ RAII_recursion thực hiện có thể là:

void RAII_recursion(int a) { 
    std::vector<int> arr(a); 
    // do some stuff with vector "arr" 
    RAII_recursion(--a); // tail-call 
}; // arr gets destroyed here, not good for TCO. 

Nhưng một lập trình viên khôn ngoan vẫn có thể thấy rằng điều này sẽ không hoạt động (trừ trường hợp destructor vector được sắp xếp theo hàng đó là khả năng trong trường hợp này) và có thể khắc phục tình huống dễ dàng:

void RAII_recursion(int a) { 
    { 
    std::vector<int> arr(a); 
    // do some stuff with vector "arr" 
    }; // arr gets destroyed here 
    RAII_recursion(--a); // tail-call 
}; 

Và tôi khá chắc chắn bạn có thể chứng minh rằng về cơ bản không có trường hợp nào lừa không thể được sử dụng để đảm bảo rằng TCO có thể được áp dụng. Vì vậy, RAII chỉ làm cho nó một chút khó khăn hơn để xem nếu TCO có thể được áp dụng. Nhưng tôi nghĩ rằng các lập trình viên đủ khôn ngoan để thiết kế các cuộc gọi đệ quy có khả năng TCO cũng đủ khôn ngoan để thấy những cuộc gọi hủy hoại "ẩn" cần phải xảy ra trước cuộc gọi đuôi.

THÊM LƯU Ý: Nhìn vào nó theo cách này, trình phá hủy ẩn đi một số mã dọn sạch tự động. Nếu bạn cần mã dọn dẹp (tức là, destructor không tầm thường), bạn sẽ cần nó cho dù bạn sử dụng RAII hay không (ví dụ: mảng kiểu C hoặc bất kỳ thứ gì). Và sau đó, nếu bạn muốn TCO có thể, nó phải có khả năng làm sạch trước khi thực hiện cuộc gọi đuôi (có hoặc không có RAII), và nó có thể, sau đó có thể buộc các đối tượng RAII bị phá hủy trước khi cuộc gọi đuôi (ví dụ, bằng cách đặt chúng bên trong một phạm vi bổ sung).

+1

Nếu destructor của vector có một tác dụng phụ bên ngoài nhìn thấy được, tôi nghĩ tiêu chuẩn C++ sẽ yêu cầu tất cả chúng được xây dựng, và sau đó tất cả được hủy theo thứ tự ngược lại, phải không? Biết liệu destructor của 'arr' có thể có bất kỳ tác dụng phụ nào sẽ yêu cầu phân tích tất cả các mã * bao giờ * nhận một con trỏ hay tham chiếu đến' arr'. Có thể có thể trong một số trường hợp, nhưng không phải tất cả. – supercat

+2

@supercat: Tiêu chuẩn xác định hành vi quan sát (nghĩa là, hoạt động "như thể" bị hủy theo thứ tự ngược lại), đó là một chút vấn đề về mèo của Schrodinger, ngay sau khi bạn thêm mã để thực hiện thứ tự thực hiện có thể quan sát được, tiêu chuẩn đảm bảo trật tự, nhưng khi bạn xóa mã đó, bạn không thể chắc chắn thứ tự của những thứ bạn không thể quan sát được. –

+1

@supercat: Đối với việc phá hủy vectơ có "tác dụng phụ có thể nhìn thấy bên ngoài", đó là tất nhiên phụ thuộc vào trình biên dịch và tình hình. Nó chắc chắn không phải lúc nào cũng được xác định. Ngoài ra, trình biên dịch thông minh như thế nào là w.r.t. mem-allocations cũng rất quan trọng (ví dụ, nó có coi nó như là một cuộc gọi hàm với các hiệu ứng phụ có thể xảy ra không, nó xem xét những thay đổi đối với vùng heap như một hiệu ứng phụ có thể nhìn thấy, vv), và các trình biên dịch có một số tự do trong cái đó. Tôi đồng ý rằng mã dọn dẹp khó có thể đặt hàng lại an toàn. –

4

Nếu trình biên dịch thực hiện TCO thì thứ tự mà trong đó destructors được gọi là thay đổi liên quan đến khi nó không làm TCO.

Nếu trình biên dịch có thể chứng minh rằng sắp xếp lại này không quan trọng (ví dụ: nếu destructor là tầm thường) thì theo quy tắc as-if nó có thể thực hiện TCO. Tuy nhiên, trong ví dụ của bạn trình biên dịch không thể chứng minh điều đó và sẽ không làm TCO.

+1

Trong trường hợp của RAII, nó sẽ không bao giờ có thể chứng minh rằng việc sắp xếp lại không quan trọng, bởi vì gần như theo định nghĩa, nó không quan trọng. –

+1

@James Kanze. Thật. Nếu chúng ta thấy RAII theo nghĩa là destructor * giải phóng tài nguyên * (nghĩa là chấp nhận được nhất của thuật ngữ) thì không thể làm TCO. Nếu chúng ta thấy RAII theo nghĩa rộng hơn, có nghĩa là các destructor được gọi khi rời khỏi phạm vi hiện tại, trong một số trường hợp, nó vẫn có thể thực hiện TCO. –

+1

Các cấu trúc không làm gì cả là _not_ RAII. Và nói chung, sự hiện diện của các nhà xây dựng không tầm thường hoặc các destructors không ức chế TCO. –