Quy tắc vàng "as-if" 1 của C ++ nói rằng, nếu hành vi quan sát được của một chương trình không phụ thuộc vào sự tồn tại của thành viên dữ liệu không được sử dụng, thì trình biên dịch được phép tối ưu hóa nó .
Một biến thành viên không sử dụng có chiếm bộ nhớ không?
Không (nếu nó "thực sự" không được sử dụng).
Bây giờ có hai câu hỏi trong đầu:
- Khi nào thì hành vi có thể quan sát được sẽ không phụ thuộc vào sự tồn tại của một thành viên?
- Những tình huống đó có xảy ra trong các chương trình đời thực không?
Hãy bắt đầu với một ví dụ.
Thí dụ
#include <iostream>
struct Foo1
{ int var1 = 5; Foo1() { std::cout << var1; } };
struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };
void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }
Nếu chúng tôi yêu cầu gcc biên dịch đơn vị dịch này , nó sẽ xuất ra:
f1():
mov esi, 5
mov edi, OFFSET FLAT:_ZSt4cout
jmp std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
jmp f1()
f2
giống như f1
, và không có bộ nhớ nào được sử dụng để chứa một thực tế Foo2::var2
. ( Clang làm điều gì đó tương tự ).
Thảo luận
Một số người có thể nói rằng điều này là khác nhau vì hai lý do:
- đây là một ví dụ quá tầm thường,
- cấu trúc được tối ưu hóa hoàn toàn, nó không được tính.
Chà, một chương trình tốt là một tổ hợp thông minh và phức tạp của những thứ đơn giản chứ không phải là sự xếp chồng đơn giản của những thứ phức tạp. Trong cuộc sống thực, bạn viết rất nhiều hàm đơn giản bằng các cấu trúc đơn giản hơn là trình biên dịch tối ưu hóa. Ví dụ:
bool insert(std::set<int>& set, int value)
{
return set.insert(value).second;
}
Đây là một ví dụ chính xác về một data-member (ở đây, std::pair<std::set<int>::iterator, bool>::first
) không được sử dụng. Đoán xem? Nó được tối ưu hóa đi ( ví dụ đơn giản hơn với một bộ giả nếu lắp ráp đó khiến bạn khóc).
Bây giờ sẽ là thời điểm hoàn hảo để đọc câu trả lời xuất sắc của Max Langhof (vui lòng ủng hộ nó cho tôi). Nó giải thích tại sao, cuối cùng, khái niệm cấu trúc không có ý nghĩa ở cấp độ lắp ráp mà trình biên dịch xuất ra.
"Nhưng, nếu tôi làm X, thực tế là thành viên không sử dụng được tối ưu hóa đi là một vấn đề!"
Đã có một số bình luận cho rằng câu trả lời này phải sai vì một số thao tác (như assert(sizeof(Foo2) == 2*sizeof(int))
) sẽ phá vỡ một cái gì đó.
Nếu X là một phần của hành vi có thể quan sát được của chương trình 2 , trình biên dịch không được phép tối ưu hóa mọi thứ. Có rất nhiều hoạt động trên một đối tượng có chứa thành phần dữ liệu "không được sử dụng" sẽ có tác động có thể quan sát được đối với chương trình. Nếu một hoạt động như vậy được thực hiện hoặc nếu trình biên dịch không thể chứng minh không có hoạt động nào được thực hiện, thì phần tử dữ liệu "không được sử dụng" đó là một phần của hành vi quan sát được của chương trình và không thể được tối ưu hóa đi .
Các hoạt động ảnh hưởng đến hành vi có thể quan sát được bao gồm, nhưng không giới hạn ở:
- lấy kích thước của một loại đối tượng (
sizeof(Foo)
),
- lấy địa chỉ của thành viên dữ liệu được khai báo sau thành viên "chưa sử dụng",
- sao chép đối tượng với một chức năng như
memcpy
,
- thao tác biểu diễn của đối tượng (như với
memcmp
),
- xác định một đối tượng là dễ bay hơi ,
- vân vân .
1)
[intro.abstract]/1
Các mô tả ngữ nghĩa trong tài liệu này xác định một máy trừu tượng không xác định được tham số hóa. Tài liệu này không yêu cầu về cấu trúc của việc triển khai tuân thủ. Đặc biệt, họ không cần sao chép hoặc mô phỏng cấu trúc của máy trừu tượng. Thay vào đó, các triển khai tuân thủ được yêu cầu để mô phỏng (chỉ) hành vi có thể quan sát được của máy trừu tượng như được giải thích bên dưới.
2) Giống như một sự khẳng định là đậu hay trượt.