Tại sao một T * có thể được thông qua trong đăng ký, nhưng unique_ptr <T> không thể?


85

Tôi đang xem cuộc nói chuyện của Chandler Carruth trong CppCon 2019:

Không có Tóm tắt chi phí bằng không

trong đó, anh ta đưa ra ví dụ về việc anh ta đã ngạc nhiên như thế nào về việc bạn phải chịu bao nhiêu chi phí bằng cách sử dụng một cái std::unique_ptr<int>trên int*; đoạn đó bắt đầu vào thời điểm 17:25.

Bạn có thể xem kết quả biên dịch của cặp đoạn trích mẫu của anh ấy (godbolt.org) - để chứng kiến ​​rằng, thực sự, có vẻ như trình biên dịch không sẵn sàng vượt qua giá trị unique_ptr - mà thực tế ở dòng dưới cùng là chỉ là một địa chỉ - bên trong một thanh ghi, chỉ trong bộ nhớ thẳng.

Một trong những điểm mà ông Carruth đưa ra vào khoảng 27:00 là C ++ ABI yêu cầu các tham số theo giá trị (một số nhưng không phải tất cả; có lẽ - các loại không nguyên thủy? Các loại không tầm thường?) Được truyền vào bộ nhớ thay vì trong một đăng ký.

Những câu hỏi của tôi:

  1. Đây thực sự là một yêu cầu ABI trên một số nền tảng? (mà?) Hoặc có thể đó chỉ là một số bi quan trong một số tình huống nhất định?
  2. Tại sao ABI lại như vậy? Đó là, nếu các trường của struct / class phù hợp với các thanh ghi, hoặc thậm chí là một thanh ghi duy nhất - tại sao chúng ta không thể vượt qua nó trong thanh ghi đó?
  3. Ủy ban tiêu chuẩn C ++ đã thảo luận về điểm này trong những năm gần đây hay chưa?

PS - Vì vậy, không để lại câu hỏi này không có mã:

Con trỏ đơn giản:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Con trỏ duy nhất:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}

8
Tôi không chắc chính xác yêu cầu ABI là gì, nhưng nó không cấm đưa các cấu trúc vào sổ đăng ký
harold

6
Nếu tôi phải đoán tôi sẽ nói nó phải làm với các hàm thành viên không tầm thường cần một thiscon trỏ trỏ đến một vị trí hợp lệ. unique_ptrcó những cái đó Làm đổ đăng ký cho mục đích đó sẽ loại bỏ toàn bộ tối ưu hóa "vượt qua trong đăng ký".
Người kể chuyện - Unslander Monica

2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . Vì vậy, hành vi này cần thiết. Tại sao? itanium-cxx-abi.github.io/cxx-abi/cxx-closes.html , tìm kiếm vấn đề C-7. Có một số lời giải thích ở đó, nhưng nó không quá chi tiết. Nhưng vâng, hành vi này có vẻ không hợp lý với tôi. Những đối tượng này có thể được chuyển qua ngăn xếp bình thường. Đẩy chúng lên ngăn xếp, và sau đó chuyển tham chiếu (chỉ cho các đối tượng "không tầm thường") có vẻ là một sự lãng phí.
geza

6
Có vẻ như C ++ đang vi phạm các nguyên tắc của chính nó ở đây, điều này khá buồn. Tôi đã bị thuyết phục 140% bất kỳ unique_ptr nào biến mất sau khi biên dịch. Rốt cuộc, nó chỉ là một cuộc gọi hủy diệt bị trì hoãn được biết đến vào thời gian biên dịch.
Biệt đội khỉ một người

7
@MaximEgorushkin: Nếu bạn đã viết nó bằng tay, bạn sẽ đặt con trỏ vào một thanh ghi chứ không phải trên ngăn xếp.
einpoklum

Câu trả lời:


49
  1. Đây thực sự là một yêu cầu ABI, hoặc có thể nó chỉ là một sự bi quan trong một số tình huống nhất định?

Một ví dụ là Giao diện nhị phân ứng dụng System64 Bổ sung bộ xử lý kiến ​​trúc AMD64 . ABI này dành cho CPU tương thích 64 bit x86 (kiến trúc Linux x86_64). Nó được theo dõi trên Solaris, Linux, FreeBSD, macOS, Hệ thống con Windows cho Linux:

Nếu một đối tượng C ++ có hàm tạo sao chép không tầm thường hoặc hàm hủy không tầm thường, thì nó được truyền bằng tham chiếu vô hình (đối tượng được thay thế trong danh sách tham số bằng một con trỏ có lớp INTEGER).

Một đối tượng với hàm tạo sao chép không tầm thường hoặc hàm hủy không tầm thường không thể được truyền bằng giá trị vì các đối tượng đó phải có địa chỉ được xác định rõ. Các vấn đề tương tự được áp dụng khi trả về một đối tượng từ một hàm.

Lưu ý, chỉ có thể sử dụng 2 thanh ghi mục đích chung để truyền 1 đối tượng với hàm tạo sao chép tầm thường và hàm hủy tầm thường, tức là chỉ các giá trị của các đối tượng sizeofkhông lớn hơn 16 có thể được truyền vào các thanh ghi. Xem các quy ước gọi của Agner Fog để biết cách xử lý chi tiết các quy ước gọi, đặc biệt là §7.1 Các đối tượng chuyển và trả lại. Có các quy ước gọi riêng để chuyển các loại SIMD trong các thanh ghi.

Có các ABI khác nhau cho các kiến ​​trúc CPU khác.


  1. Tại sao ABI lại như vậy? Đó là, nếu các trường của struct / class phù hợp với các thanh ghi, hoặc thậm chí là một thanh ghi duy nhất - tại sao chúng ta không thể vượt qua nó trong thanh ghi đó?

Đây là một chi tiết triển khai, nhưng khi một ngoại lệ được xử lý, trong quá trình giải nén ngăn xếp, các đối tượng có thời gian lưu trữ tự động bị phá hủy phải có địa chỉ liên quan đến khung ngăn xếp chức năng vì các thanh ghi đã bị chặn bởi thời gian đó. Mã giải nén ngăn xếp cần địa chỉ của các đối tượng để gọi các hàm hủy của chúng nhưng các đối tượng trong các thanh ghi không có địa chỉ.

Về mặt giáo dục, các tàu khu trục hoạt động trên các đối tượng :

Một đối tượng chiếm một vùng lưu trữ trong thời kỳ xây dựng ([class.cdtor]), trong suốt vòng đời của nó và trong thời kỳ hủy diệt.

và một đối tượng không thể tồn tại trong C ++ nếu không có bộ nhớ địa chỉ nào được phân bổ cho nó vì danh tính của đối tượng là địa chỉ của nó .

Khi cần một địa chỉ của một đối tượng với hàm tạo sao chép tầm thường trong các thanh ghi, trình biên dịch chỉ có thể lưu trữ đối tượng vào bộ nhớ và lấy địa chỉ. Nếu hàm tạo sao chép là không tầm thường, mặt khác, trình biên dịch không thể lưu trữ nó vào bộ nhớ, nó cần gọi hàm tạo sao chép để tham chiếu và do đó yêu cầu địa chỉ của đối tượng trong các thanh ghi. Quy ước gọi có lẽ không thể phụ thuộc vào việc hàm tạo sao chép có được đặt trong callee hay không.

Một cách khác để suy nghĩ về điều này, là đối với các loại có thể sao chép tầm thường, trình biên dịch sẽ chuyển giá trị của một đối tượng trong các thanh ghi, từ đó một đối tượng có thể được phục hồi bằng các bộ nhớ đơn giản nếu cần thiết. Ví dụ:

void f(long*);
void g(long a) { f(&a); }

trên x86_64 với System V ABI biên dịch thành:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

Trong bài nói chuyện kích thích tư duy của mình, Chandler Carruth đã đề cập rằng một sự thay đổi ABI phá vỡ có thể là cần thiết (trong số những thứ khác) để thực hiện động thái phá hoại có thể cải thiện mọi thứ. IMO, thay đổi ABI có thể không phá vỡ nếu các chức năng sử dụng ABI mới chọn tham gia rõ ràng để có một liên kết khác mới, ví dụ: khai báo chúng trongextern "C++20" {} khối (có thể, trong một không gian tên nội tuyến mới để di chuyển các API hiện có). Vì vậy, chỉ có mã được biên dịch theo các khai báo hàm mới với liên kết mới có thể sử dụng ABI mới.

Lưu ý rằng ABI không áp dụng khi hàm được gọi đã được nội tuyến. Cũng như với việc tạo mã thời gian liên kết, trình biên dịch có thể các hàm nội tuyến được xác định trong các đơn vị dịch thuật khác hoặc sử dụng các quy ước gọi tùy chỉnh.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

8

Với ABI thông thường, hàm hủy không tầm thường -> không thể vượt qua trong các thanh ghi

(Một minh họa về một điểm trong câu trả lời của @ MaximEgorushkin bằng cách sử dụng ví dụ của @ harold trong một nhận xét; được sửa theo nhận xét của @ Yakk.)

Nếu bạn biên dịch:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

bạn lấy:

test(Foo):
        mov     eax, edi
        ret

tức là Foođối tượng được truyền testvào một thanh ghi ( edi) và cũng được trả về trong một thanh ghi ( eax).

Khi hàm hủy không tầm thường (như std::unique_ptr ví dụ về OP) - ABI thông thường yêu cầu vị trí trên ngăn xếp. Điều này đúng ngay cả khi hàm hủy hoàn toàn không sử dụng địa chỉ của đối tượng.

Do đó, ngay cả trong trường hợp cực đoan của hàm hủy không làm gì, nếu bạn biên dịch:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

bạn lấy:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

với tải và lưu trữ vô dụng.


Tôi không bị thuyết phục bởi lập luận này. Hàm hủy không tầm thường không làm gì để cấm quy tắc as-if. Nếu địa chỉ không được quan sát, hoàn toàn không có lý do tại sao cần phải có một địa chỉ. Vì vậy, một trình biên dịch tuân thủ có thể vui vẻ đưa nó vào một thanh ghi, nếu làm như vậy không làm thay đổi hành vi có thể quan sát được (và các trình biên dịch hiện tại trên thực tế sẽ làm như vậy nếu người gọi được biết ).
ComicSansMS

1
Thật không may, đó là cách khác (tôi đồng ý rằng một số điều này đã vượt quá lý trí). Nói chính xác: Tôi không tin rằng những lý do bạn cung cấp nhất thiết sẽ đưa ra bất kỳ ABI có thể hiểu được nào cho phép vượt qua dòng điện std::unique_ptrtrong một đăng ký không tuân thủ.
ComicSansMS

3
"Kẻ hủy diệt tầm thường [NHU CẦU CẦN THIẾT]" rõ ràng sai; nếu không có mã nào thực sự phụ thuộc vào địa chỉ, thì as-if có nghĩa là địa chỉ không cần tồn tại trên máy thực tế . Địa chỉ phải tồn tại trong máy trừu tượng , nhưng những thứ trong máy trừu tượng không có tác động đến máy thực tế là những thứ như thể được phép loại bỏ.
Yakk - Adam Nevraumont

2
@einpoklum Không có gì trong tiêu chuẩn mà các thanh ghi trạng thái tồn tại. Từ khóa đăng ký chỉ ghi "bạn không thể lấy địa chỉ". Chỉ có một máy trừu tượng theo như tiêu chuẩn có liên quan. "Như thể" có nghĩa là bất kỳ việc thực hiện máy thực sự nào cũng chỉ cần hành xử "như thể" máy trừu tượng hoạt động, cho đến hành vi không được xác định bởi tiêu chuẩn. Bây giờ, có những vấn đề rất thách thức xung quanh việc có một đối tượng trong một đăng ký, mà mọi người đã nói về rộng rãi. Ngoài ra, các quy ước gọi, mà tiêu chuẩn cũng không thảo luận, có nhu cầu thực tế.
Yakk - Adam Nevraumont

1
@einpoklum Không, trong cỗ máy trừu tượng đó, tất cả mọi thứ đều có địa chỉ; nhưng địa chỉ chỉ có thể quan sát trong một số trường hợp nhất định. Các registertừ khóa được dự định để làm cho nó tầm thường đối với máy vật lý để lưu trữ một cái gì đó trong một thanh ghi bằng cách ngăn chặn những điều mà thực tế làm cho nó khó khăn hơn để "không có địa chỉ" trong máy vật lý.
Yakk - Adam Nevraumont

2

Đây thực sự là một yêu cầu ABI trên một số nền tảng? (mà?) Hoặc có thể đó chỉ là một số bi quan trong một số tình huống nhất định?

Nếu một cái gì đó có thể nhìn thấy ở bộ phận đơn vị khiếu nại thì nó được định nghĩa ngầm hay rõ ràng nó trở thành một phần của ABI.

Tại sao ABI lại như vậy?

Vấn đề cơ bản là các thanh ghi được lưu và khôi phục mọi lúc khi bạn di chuyển xuống và lên ngăn xếp cuộc gọi. Vì vậy, nó không thực tế để có một tham chiếu hoặc con trỏ đến chúng.

Nội tuyến và tối ưu hóa kết quả từ nó là tốt khi nó xảy ra, nhưng một nhà thiết kế ABI không thể dựa vào nó xảy ra. Họ phải thiết kế ABI giả sử trường hợp xấu nhất. Tôi không nghĩ các lập trình viên sẽ rất hài lòng với trình biên dịch mà ABI thay đổi tùy thuộc vào mức độ tối ưu hóa.

Một loại có thể sao chép tầm thường có thể được thông qua trong các thanh ghi vì hoạt động sao chép logic có thể được chia thành hai phần. Các tham số được sao chép vào các thanh ghi được sử dụng để truyền tham số cho người gọi và sau đó sao chép vào biến cục bộ bằng callee. Cho dù biến cục bộ có vị trí bộ nhớ hay không thì đó chỉ là mối quan tâm của callee.

Một loại trong đó một công cụ sao chép hoặc di chuyển phải được sử dụng mặt khác không thể có hoạt động sao chép được phân tách theo cách này, vì vậy nó phải được truyền vào bộ nhớ.

Ủy ban tiêu chuẩn C ++ đã thảo luận về điểm này trong những năm gần đây hay chưa?

Tôi không biết nếu các cơ quan tiêu chuẩn đã xem xét điều này.

Giải pháp rõ ràng với tôi là thêm các động thái phá hoại thích hợp (chứ không phải là ngôi nhà nửa chừng hiện tại của "trạng thái hợp lệ nhưng không xác định") vào ngôn ngữ, sau đó giới thiệu một cách để gắn cờ một loại như cho phép "di chuyển phá hoại tầm thường "Ngay cả khi nó không cho phép các bản sao tầm thường.

nhưng một giải pháp như vậy sẽ yêu cầu phá vỡ ABI của mã hiện tại để triển khai cho các loại hiện có, điều này có thể mang lại một chút kháng cự (mặc dù ABI bị phá vỡ do các phiên bản tiêu chuẩn C ++ mới không phải là chưa từng có, ví dụ như thay đổi chuỗi std :: trong C ++ 11 dẫn đến phá vỡ ABI ..


Bạn có thể giải thích về cách di chuyển phá hoại thích hợp sẽ cho phép một unique_ptr được thông qua trong một thanh ghi không? Đó có phải là vì nó sẽ cho phép bỏ yêu cầu lưu trữ theo địa chỉ?
einpoklum

Di chuyển phá hoại thích hợp sẽ cho phép một khái niệm về di chuyển phá hoại tầm thường được giới thiệu. Điều này sẽ cho phép di chuyển tầm thường được chia tách bởi ABI theo cùng cách mà các bản sao tầm thường có thể có ngày nay.
cắm vào

Mặc dù bạn cũng muốn thêm một quy tắc rằng trình biên dịch có thể thực hiện truyền tham số dưới dạng di chuyển thông thường hoặc sao chép theo sau là "di chuyển phá hủy tầm thường" để đảm bảo rằng luôn có thể vượt qua các thanh ghi cho dù tham số đó đến từ đâu.
cắm vào

Bởi vì kích thước thanh ghi có thể chứa một con trỏ, nhưng cấu trúc unique_ptr? Kích thước nào (unique_ptr <T>)?
Mel Viso Martinez

@MelVisoMartinez Bạn có thể nhầm lẫn unique_ptrshared_ptrngữ nghĩa: shared_ptr<T>cho phép bạn cung cấp cho ctor 1) một ptr x cho đối tượng dẫn xuất U bị xóa bằng loại tĩnh U w / biểu thức delete x;(vì vậy bạn không cần một trình giả ảo ở đây) 2) hoặc thậm chí là một chức năng dọn dẹp tùy chỉnh. Điều đó có nghĩa là trạng thái thời gian chạy được sử dụng bên trong shared_ptrkhối điều khiển để mã hóa thông tin đó. OTOH unique_ptrkhông có chức năng như vậy và không mã hóa hành vi xóa trong trạng thái; cách duy nhất để tùy chỉnh dọn dẹp là tạo một bản mẫu khác (loại lớp khác).
tò mò

-1

Đầu tiên chúng ta cần quay trở lại ý nghĩa của việc vượt qua giá trị và tham chiếu.

Đối với các ngôn ngữ như Java và SML, việc truyền theo giá trị rất đơn giản (và không có thông qua tham chiếu), giống như sao chép một giá trị biến, vì tất cả các biến chỉ là vô hướng và có bản sao ngữ nghĩa: chúng là những gì được coi là số học gõ vào C ++ hoặc "tài liệu tham khảo" (con trỏ với tên và cú pháp khác nhau).

Trong C, chúng ta có các kiểu vô hướng và người dùng xác định:

  • Vô hướng có một giá trị số hoặc trừu tượng (con trỏ không phải là số, chúng có giá trị trừu tượng) được sao chép.
  • Các loại tổng hợp có tất cả các thành viên có thể khởi tạo được sao chép:
    • đối với các loại sản phẩm (mảng và cấu trúc): theo cách đệ quy, tất cả các thành viên của cấu trúc và thành phần của mảng được sao chép (cú pháp hàm C không thể truyền trực tiếp mảng theo giá trị, chỉ mảng thành viên của cấu trúc, nhưng đó là chi tiết ).
    • đối với các loại tổng (công đoàn): giá trị của "thành viên tích cực" được giữ nguyên; rõ ràng, thành viên của bản sao thành viên không theo thứ tự vì không phải tất cả các thành viên có thể được khởi tạo.

Trong C ++, các kiểu do người dùng xác định có thể có ngữ nghĩa sao chép do người dùng xác định, cho phép lập trình thực sự "hướng đối tượng" với các đối tượng có quyền sở hữu tài nguyên của họ và các hoạt động "sao chép sâu". Trong trường hợp như vậy, một hoạt động sao chép thực sự là một cuộc gọi đến một chức năng gần như có thể thực hiện các hoạt động tùy ý.

Đối với các cấu trúc C được biên dịch là C ++, "sao chép" vẫn được định nghĩa là gọi hoạt động sao chép do người dùng định nghĩa (hàm tạo hoặc toán tử gán), được trình biên dịch ngầm tạo. Điều đó có nghĩa là ngữ nghĩa của chương trình tập hợp con chung C / C ++ khác nhau ở C và C ++: trong C, toàn bộ loại tổng hợp được sao chép, trong C ++, một hàm sao chép được tạo ngầm được gọi để sao chép từng thành viên; kết quả cuối cùng là trong cả hai trường hợp, mỗi thành viên được sao chép.

(Tôi nghĩ có một ngoại lệ, khi một cấu trúc bên trong một liên minh được sao chép.)

Vì vậy, đối với một loại lớp, cách duy nhất (bên ngoài các bản sao hợp nhất) để tạo một thể hiện mới là thông qua một hàm tạo (ngay cả đối với những người có trình biên dịch tạo trình biên dịch tầm thường).

Bạn không thể lấy địa chỉ của một giá trị thông qua toán tử đơn nguyên &nhưng điều đó không có nghĩa là không có đối tượng giá trị; và một đối tượng, theo định nghĩa, có một địa chỉ ; và địa chỉ đó thậm chí được biểu thị bằng một cấu trúc cú pháp: một đối tượng của kiểu lớp chỉ có thể được tạo bởi một hàm tạo và nó có một thiscon trỏ; nhưng đối với các loại tầm thường, không có hàm tạo bằng văn bản người dùng nên không có chỗ để đặt thischo đến sau khi bản sao được tạo và đặt tên.

Đối với kiểu vô hướng, giá trị của một đối tượng là giá trị của đối tượng, giá trị toán học thuần túy được lưu trữ vào đối tượng.

Đối với một loại lớp, khái niệm duy nhất về giá trị của đối tượng là một bản sao khác của đối tượng, chỉ có thể được tạo bởi một hàm tạo sao chép, một hàm thực (mặc dù đối với các loại tầm thường có chức năng rất đặc biệt, đôi khi chúng có thể là được tạo mà không gọi hàm tạo). Điều đó có nghĩa là giá trị của đối tượng là kết quả của sự thay đổi trạng thái chương trình toàn cầu bằng cách thực thi . Nó không truy cập toán học.

Vì vậy, vượt qua giá trị thực sự không phải là một điều: nó vượt qua cuộc gọi của nhà xây dựng sao chép , điều này ít đẹp hơn. Hàm tạo sao chép dự kiến ​​sẽ thực hiện thao tác "sao chép" hợp lý theo ngữ nghĩa thích hợp của loại đối tượng, tôn trọng các bất biến bên trong của nó (là các thuộc tính người dùng trừu tượng, không phải thuộc tính C ++ nội tại).

Truyền theo giá trị của một đối tượng lớp có nghĩa là:

  • tạo một ví dụ khác
  • sau đó làm cho hàm được gọi hành động trong trường hợp đó.

Lưu ý rằng vấn đề không liên quan đến việc bản sao có phải là một đối tượng có địa chỉ hay không: tất cả các tham số chức năng là đối tượng và có địa chỉ (ở cấp ngữ nghĩa ngôn ngữ).

Vấn đề là liệu:

  • bản sao là một đối tượng mới được khởi tạo với giá trị toán học thuần túy (giá trị thuần thực) của đối tượng ban đầu, như với vô hướng;
  • hoặc bản sao là giá trị của đối tượng gốc , như với các lớp.

Trong trường hợp của loại lớp tầm thường, bạn vẫn có thể xác định thành viên của bản sao thành viên của bản gốc, do đó bạn có thể xác định giá trị thuần của bản gốc vì tính tầm thường của các hoạt động sao chép (hàm tạo sao chép và gán). Không phải như vậy với các chức năng người dùng đặc biệt tùy ý: một giá trị của bản gốc phải là một bản sao được xây dựng.

Các đối tượng lớp phải được xây dựng bởi người gọi; một hàm tạo chính thức có một thiscon trỏ nhưng chủ nghĩa hình thức không liên quan ở đây: tất cả các đối tượng chính thức có một địa chỉ nhưng chỉ những đối tượng thực sự sử dụng địa chỉ của chúng theo cách không thuần túy cục bộ (không giống như *&i = 1;sử dụng địa chỉ thuần túy địa phương) cần phải được xác định rõ Địa chỉ.

Một đối tượng phải hoàn toàn bằng cách chuyển qua địa chỉ nếu nó phải xuất hiện một địa chỉ trong cả hai hàm được biên dịch riêng biệt này:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

Ở đây ngay cả khi something(address)là một hàm thuần túy hoặc macro hoặc bất cứ thứ gì (như printf("%p",arg)) không thể lưu trữ địa chỉ hoặc liên lạc với thực thể khác, chúng tôi có yêu cầu chuyển qua địa chỉ vì địa chỉ phải được xác định rõ cho một đối tượng duy nhất intcó duy nhất danh tính.

Chúng tôi không biết nếu một chức năng bên ngoài sẽ "thuần túy" về mặt địa chỉ được truyền cho nó.

Ở đây, tiềm năng sử dụng thực sự của địa chỉ trong một hàm tạo không phải là tầm thường hoặc hàm hủy ở phía người gọi có lẽ là lý do để lấy tuyến đường an toàn, đơn giản và cung cấp cho đối tượng một danh tính trong người gọi và chuyển địa chỉ của nó, vì nó tạo ra chắc chắn rằng bất kỳ việc sử dụng không tầm thường của địa chỉ của nó trong hàm tạo, sau khi xây dựng và trong hàm hủy là nhất quán : thisphải xuất hiện giống nhau trên sự tồn tại của đối tượng.

Một hàm tạo hoặc hàm hủy không tầm thường như bất kỳ hàm nào khác có thể sử dụng thiscon trỏ theo cách đòi hỏi tính nhất quán đối với giá trị của nó mặc dù một số đối tượng có nội dung không tầm thường có thể không:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

Lưu ý rằng trong trường hợp đó, mặc dù sử dụng rõ ràng một con trỏ (cú pháp rõ ràng this->), danh tính đối tượng là không liên quan: trình biên dịch cũng có thể sử dụng sao chép từng bit đối tượng xung quanh để di chuyển nó và thực hiện "sao chép elision". Điều này dựa trên mức độ "tinh khiết" của việc sử dụng các thischức năng thành viên đặc biệt (địa chỉ không thoát).

Nhưng độ tinh khiết không phải là một thuộc tính có sẵn ở cấp khai báo tiêu chuẩn (phần mở rộng trình biên dịch tồn tại thêm mô tả độ tinh khiết vào khai báo hàm không nội tuyến), vì vậy bạn không thể xác định ABI dựa trên độ tinh khiết của mã có thể không có (mã có thể hoặc có thể không phải là nội tuyến và có sẵn để phân tích).

Độ tinh khiết được đo là "chắc chắn tinh khiết" hoặc "không tinh khiết hoặc không rõ". Điểm chung, hoặc giới hạn trên của ngữ nghĩa (thực tế tối đa), hoặc LCM (Least Common bội) là "không xác định". Vì vậy, ABI giải quyết trên chưa biết.

Tóm lược:

  • Một số cấu trúc yêu cầu trình biên dịch xác định danh tính đối tượng.
  • ABI được định nghĩa theo thuật ngữ của các lớp chương trình và không phải là trường hợp cụ thể có thể được tối ưu hóa.

Công việc có thể trong tương lai:

Chú thích độ tinh khiết có đủ hữu ích để được khái quát hóa và tiêu chuẩn hóa không?


1
Ví dụ đầu tiên của bạn xuất hiện sai lệch. Tôi nghĩ rằng bạn chỉ đang đưa ra một quan điểm chung chung, nhưng ban đầu tôi nghĩ bạn đang tạo ra sự tương đồng với mã trong câu hỏi. Nhưng void foo(unique_ptr<int> ptr)lấy đối tượng lớp theo giá trị . Đối tượng đó có một thành viên con trỏ, nhưng chúng ta đang nói về chính đối tượng lớp được truyền bằng tham chiếu. (Vì nó không thể sao chép một cách tầm thường nên hàm tạo / hàm hủy của nó cần nhất quán this.) Đó là đối số thực và không được kết nối với ví dụ đầu tiên chuyển qua tham chiếu một cách rõ ràng ; trong trường hợp đó, con trỏ được truyền vào một thanh ghi.
Peter Cordes

@PeterCordes " bạn đã tạo ra sự tương tự với mã trong câu hỏi. " Tôi đã làm chính xác điều đó. " Đối tượng lớp theo giá trị " Có, tôi có lẽ nên giải thích rằng nói chung không có thứ gọi là "giá trị" của một đối tượng lớp vì vậy theo giá trị cho một loại toán không phải là "theo giá trị". " Đối tượng đó có một thành viên con trỏ " Bản chất giống như ptr của "ptr thông minh" là không liên quan; và thành viên ptr của "ptr thông minh" cũng vậy. Một ptr chỉ là một vô hướng như int: Tôi đã viết một ví dụ "fileno thông minh" minh họa rằng "quyền sở hữu" không liên quan gì đến việc "mang ptr".
tò mò

1
Giá trị của một đối tượng lớp là biểu diễn đối tượng của nó. Đối với unique_ptr<T*>, đây là kích thước và bố cục tương tự T*và phù hợp với một thanh ghi. Các đối tượng lớp có thể sao chép tầm thường có thể được truyền bằng giá trị trong các thanh ghi trong x86-64 System V, giống như hầu hết các quy ước gọi. Điều này tạo ra một bản sao của unique_ptrđối tượng, không giống như trong intví dụ của bạn trong đó callee &i địa chỉ của người gọi ivì bạn đã chuyển qua tham chiếu ở cấp độ C ++ , không chỉ là chi tiết triển khai asm.
Peter Cordes

1
Err, sửa cho nhận xét cuối cùng của tôi. Nó không chỉ tạo ra một bản sao của unique_ptrđối tượng; Nó đang sử dụng std::movenên việc sao chép nó là an toàn vì điều đó sẽ không dẫn đến 2 bản sao giống nhau unique_ptr. Nhưng đối với một loại có thể sao chép tầm thường, vâng, nó sao chép toàn bộ đối tượng tổng hợp. Nếu đó là một thành viên duy nhất, các quy ước gọi tốt sẽ coi nó giống như một vô hướng của loại đó.
Peter Cordes

1
Trông tốt hơn. Lưu ý: Đối với các cấu trúc C được biên dịch thành C ++ - Đây không phải là cách hữu ích để giới thiệu sự khác biệt giữa C ++. Trong C ++ struct{}là cấu trúc C ++. Có lẽ bạn nên nói "cấu trúc đơn giản" hoặc "không giống như C". Bởi vì, có một sự khác biệt. Nếu bạn sử dụng atomic_intnhư một thành viên cấu trúc, C sẽ sao chép nó không nguyên tử, lỗi C ++ trên hàm tạo sao chép bị xóa. Tôi quên những gì C ++ làm trên cấu trúc với volatilecác thành viên. C sẽ cho phép bạn làm struct tmp = volatile_struct;để sao chép toàn bộ nội dung (hữu ích cho SeqLock); C ++ sẽ không.
Peter Cordes
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.