Việc xử lý trình biên dịch của các biến giao diện không tường minh có được ghi lại không?


86

Tôi đã hỏi một câu hỏi tương tự về các biến giao diện ngầm cách đây không lâu.

Nguồn gốc của câu hỏi này là một lỗi trong mã của tôi do tôi không nhận thức được sự tồn tại của một biến giao diện ngầm được tạo bởi trình biên dịch. Biến này đã được hoàn tất khi thủ tục sở hữu nó hoàn tất. Điều này lại gây ra lỗi do thời gian tồn tại của biến lâu hơn tôi dự đoán.

Bây giờ, tôi có một dự án đơn giản để minh họa một số hành vi thú vị từ trình biên dịch:

program ImplicitInterfaceLocals;

{$APPTYPE CONSOLE}

uses
  Classes;

function Create: IInterface;
begin
  Result := TInterfacedObject.Create;
end;

procedure StoreToLocal;
var
  I: IInterface;
begin
  I := Create;
end;

procedure StoreViaPointerToLocal;
var
  I: IInterface;
  P: ^IInterface;
begin
  P := @I;
  P^ := Create;
end;

begin
  StoreToLocal;
  StoreViaPointerToLocal;
end.

StoreToLocalđược biên dịch giống như bạn tưởng tượng. Biến cục bộ I, kết quả của hàm, được truyền dưới dạng một vartham số ngầm định cho Create. Sắp xếp gọn gàng cho StoreToLocalkết quả trong một cuộc gọi đến IntfClear. Không có gì ngạc nhiên ở đó.

Tuy nhiên, StoreViaPointerToLocalđược đối xử khác nhau. Trình biên dịch tạo ra một biến cục bộ ngầm mà nó chuyển đến Create. Khi Createtrả về, việc gán cho P^được thực hiện. Điều này để lại thói quen với hai biến cục bộ giữ các tham chiếu đến giao diện. Dọn dẹp cho StoreViaPointerToLocalkết quả trong hai cuộc gọi đến IntfClear.

Mã đã biên dịch cho StoreViaPointerToLocalnhư thế này:

ImplicitInterfaceLocals.dpr.24: begin
00435C50 55               push ebp
00435C51 8BEC             mov ebp,esp
00435C53 6A00             push $00
00435C55 6A00             push $00
00435C57 6A00             push $00
00435C59 33C0             xor eax,eax
00435C5B 55               push ebp
00435C5C 689E5C4300       push $00435c9e
00435C61 64FF30           push dword ptr fs:[eax]
00435C64 648920           mov fs:[eax],esp
ImplicitInterfaceLocals.dpr.25: P := @I;
00435C67 8D45FC           lea eax,[ebp-$04]
00435C6A 8945F8           mov [ebp-$08],eax
ImplicitInterfaceLocals.dpr.26: P^ := Create;
00435C6D 8D45F4           lea eax,[ebp-$0c]
00435C70 E873FFFFFF       call Create
00435C75 8B55F4           mov edx,[ebp-$0c]
00435C78 8B45F8           mov eax,[ebp-$08]
00435C7B E81032FDFF       call @IntfCopy
ImplicitInterfaceLocals.dpr.27: end;
00435C80 33C0             xor eax,eax
00435C82 5A               pop edx
00435C83 59               pop ecx
00435C84 59               pop ecx
00435C85 648910           mov fs:[eax],edx
00435C88 68A55C4300       push $00435ca5
00435C8D 8D45F4           lea eax,[ebp-$0c]
00435C90 E8E331FDFF       call @IntfClear
00435C95 8D45FC           lea eax,[ebp-$04]
00435C98 E8DB31FDFF       call @IntfClear
00435C9D C3               ret 

Tôi có thể đoán tại sao trình biên dịch lại làm điều này. Khi nó có thể chứng minh rằng việc gán cho biến kết quả sẽ không tạo ra ngoại lệ (tức là nếu biến là cục bộ) thì nó sử dụng trực tiếp biến kết quả. Nếu không, nó sử dụng cục bộ ngầm định và sao chép giao diện khi hàm đã trả về, do đó đảm bảo rằng chúng tôi không làm rò rỉ tham chiếu trong trường hợp có ngoại lệ.

Nhưng tôi không thể tìm thấy bất kỳ tuyên bố nào về điều này trong tài liệu. Nó quan trọng bởi vì thời gian tồn tại của giao diện rất quan trọng và là một lập trình viên, bạn cần phải có khả năng tác động đến nó.

Vì vậy, không ai biết nếu có bất kỳ tài liệu về hành vi này? Nếu không có ai có thêm bất kỳ kiến ​​thức về nó? Các trường mẫu được xử lý như thế nào, tôi vẫn chưa kiểm tra điều đó. Tất nhiên tôi có thể tự mình thử tất cả nhưng tôi đang tìm kiếm một tuyên bố chính thức hơn và luôn muốn tránh dựa vào chi tiết triển khai được thực hiện bằng cách thử và sai.

Cập nhật 1

Để trả lời câu hỏi của Remy, điều quan trọng đối với tôi là khi tôi cần hoàn thiện đối tượng đằng sau giao diện trước khi thực hiện một lần hoàn thiện khác.

begin
  AcquirePythonGIL;
  try
    PyObject := CreatePythonObject;
    try
      //do stuff with PyObject
    finally
      Finalize(PyObject);
    end;
  finally
    ReleasePythonGIL;
  end;
end;

Như được viết như thế này, nó là tốt. Nhưng trong mã thực, tôi có một cục bộ ngầm thứ hai được hoàn thiện sau khi GIL được phát hành và nó bị đánh bom. Tôi đã giải quyết vấn đề bằng cách trích xuất mã bên trong GIL Acquire / Release thành một phương pháp riêng biệt và do đó thu hẹp phạm vi của biến giao diện.


8
Không biết tại sao điều này lại bị phản đối, ngoài việc câu hỏi thực sự phức tạp. Được ủng hộ vì đã vượt qua đầu của tôi. Tôi biết rằng chính xác bit arcanum này đã dẫn đến một số lỗi đếm tham chiếu tinh vi trong một ứng dụng tôi đã làm việc một năm trước. Một trong những chuyên gia giỏi nhất của chúng tôi đã dành hàng giờ để tìm ra nó. Cuối cùng, chúng tôi đã làm việc xung quanh nó nhưng không bao giờ hiểu được cách thức hoạt động của trình biên dịch.
Warren P

3
@Serg Trình biên dịch đã thực hiện việc đếm tham chiếu của nó một cách hoàn hảo. Vấn đề là có một biến phụ giữ một tham chiếu mà tôi không thể nhìn thấy. Những gì tôi muốn biết là điều gì kích động trình biên dịch lấy một tham chiếu bổ sung, ẩn, như vậy.
David Heffernan 13/10/11

3
Tôi hiểu bạn, nhưng một phương pháp hay là viết mã không phụ thuộc vào các biến phụ như vậy. Hãy để trình biên dịch tạo các biến này bao nhiêu tùy thích, mã rắn không nên phụ thuộc vào nó.
kludg

2
Một ví dụ khác khi điều này xảy ra:procedure StoreViaAbsoluteToLocal; var I: IInterface; I2: IInterface absolute I; begin I2 := Create; end;
Ondrej Kelle

2
Tôi muốn gọi đây là một lỗi trình biên dịch ... các khoảng thời gian tạm thời sẽ được xóa sau khi chúng vượt ra khỏi phạm vi, đó sẽ là phần cuối của nhiệm vụ (chứ không phải kết thúc của hàm). Không làm như vậy sẽ tạo ra các lỗi nhỏ như bạn đã phát hiện.
nneonneo

Câu trả lời:


15

Nếu có bất kỳ tài liệu nào về hành vi này, nó có thể nằm trong lĩnh vực sản xuất trình biên dịch của các biến tạm thời để giữ các kết quả trung gian khi chuyển kết quả hàm dưới dạng tham số. Hãy xem xét mã này:

procedure UseInterface(foo: IInterface);
begin
end;

procedure Test()
begin
    UseInterface(Create());
end;

Trình biên dịch phải tạo một biến tạm thời ngầm định để giữ kết quả của Tạo khi nó được truyền vào UseInterface, để đảm bảo rằng giao diện có thời gian tồn tại> = thời gian tồn tại của lệnh gọi UseInterface. Biến tạm thời tiềm ẩn đó sẽ được xử lý ở cuối thủ tục sở hữu nó, trong trường hợp này là ở cuối thủ tục Test ().

Có thể trường hợp gán con trỏ của bạn có thể rơi vào cùng một nhóm khi chuyển các giá trị giao diện trung gian làm tham số hàm, vì trình biên dịch không thể "thấy" giá trị đang đi đến đâu.

Tôi nhớ đã có một vài lỗi trong lĩnh vực này trong những năm qua. Cách đây khá lâu (D3? D4?), Trình biên dịch không tính giá trị trung gian cho tham chiếu. Nó hoạt động hầu hết thời gian, nhưng gặp rắc rối trong các tình huống bí danh tham số. Khi điều đó đã được giải quyết, tôi tin rằng sẽ có thông tin liên quan đến const params. Luôn có mong muốn di chuyển việc loại bỏ giao diện giá trị trung gian càng sớm càng tốt sau câu lệnh mà nó cần thiết, nhưng tôi không nghĩ rằng điều đó đã từng được triển khai trong trình tối ưu hóa Win32 bởi vì trình biên dịch chưa được thiết lập để xử lý việc thải bỏ theo tuyên bố hoặc mức độ chi tiết của khối.


0

Bạn không thể đảm bảo rằng trình biên dịch sẽ không quyết định tạo một biến vô hình tạm thời.

Và ngay cả khi bạn làm vậy, việc tắt tối ưu hóa (hoặc thậm chí cả khung xếp chồng?) Có thể làm rối mã được kiểm tra hoàn hảo của bạn.

Và ngay cả khi bạn quản lý để xem lại mã của mình theo tất cả các kết hợp có thể có của các tùy chọn dự án - việc biên dịch mã của bạn theo thứ gì đó như Lazarus hoặc thậm chí phiên bản Delphi mới sẽ khiến địa ngục trở lại.

Đặt cược tốt nhất là sử dụng quy tắc "biến nội bộ không thể tồn tại lâu hơn quy trình". Chúng ta thường không biết liệu trình biên dịch có tạo ra một số biến nội bộ hay không, nhưng chúng ta biết rằng bất kỳ biến nào như vậy (nếu được tạo) sẽ được hoàn thiện khi quy trình tồn tại.

Do đó, nếu bạn có mã như thế này:

// 1. Some code which may (or may not) create invisible variables
// 2. Some code which requires release of reference-counted data

Ví dụ:

Lib := LoadLibrary(Lib, 'xyz');
try
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- May be not OK
end;

Sau đó, bạn chỉ nên bọc khối "Làm việc với giao diện" vào chương trình con:

procedure Work(const Lib: HModule);
begin
  // Create interface
  P := GetProcAddress(Lib, 'xyz');
  I := P;
  // Work with interface
end; // <- Releases hidden variables (if any exist)

Lib := LoadLibrary(Lib, 'xyz');
try
  Work(Lib);
finally
  // Something that requires all interfaces to be released
  FreeLibrary(Lib); // <- OK!
end;

Đó là một quy tắc đơn giản, nhưng hiệu quả.


Trong trường hợp của tôi, I: = CreateInterfaceFromLib (...) dẫn đến một cục bộ ngầm định. Vì vậy, những gì bạn đề xuất sẽ không giúp ích. Trong mọi trường hợp, tôi đã chứng minh rõ ràng một cách giải quyết trong câu hỏi. Một dựa trên thời gian tồn tại của các địa phương ngầm được kiểm soát bởi phạm vi chức năng. Câu hỏi của tôi liên quan đến các kịch bản sẽ dẫn đến những người dân địa phương ngầm.
David Heffernan

Quan điểm của tôi là ngay từ đầu đây là một câu hỏi sai.
Alex

1
Bạn hoan nghênh quan điểm đó nhưng bạn nên thể hiện nó dưới dạng nhận xét. Việc thêm mã cố gắng (không thành công) để tái tạo các cách giải quyết của câu hỏi, có vẻ kỳ lạ đối với tôi.
David Heffernan
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.