Đầ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 this
con 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 this
cho đế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 this
con 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 int
có 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 : this
phả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 this
con 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 this
chứ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?