2012-01-06 33 views
22

Theo "How to get around the warning "rvalue used as lvalue"?", Visual Studio sẽ chỉ cảnh báo trên mã như thế này:Tại sao lại bất hợp pháp để lấy địa chỉ của một giá trị tạm thời?

int bar() { 
    return 3; 
} 

void foo(int* ptr) { 

} 

int main() { 
    foo(&bar()); 
} 

Trong C++ nó được không được phép để lấy địa chỉ của một tạm thời (hoặc, ít nhất, của một đối tượng được giới thiệu bởi một biểu thức rvalue?) và tôi nghĩ rằng điều này là do thời gian không được đảm bảo để thậm chí có bộ nhớ.

Nhưng sau đó, mặc dù chẩn đoán có thể được trình bày trong bất kỳ hình thức trình biên dịch chọn, tôi muốn vẫn mong đợi MSVS để lỗi hơn cảnh báo trong một trường hợp như vậy.

Vì vậy, các thời gian có được bảo đảm để lưu trữ không? Và nếu có, tại sao mã trên không được phép ở nơi đầu tiên?

+0

** Liên quan: ** http://stackoverflow.com/questions/4301179/why-is-taking-the-address-of-a-temporary-illegal (mặc dù tôi không hoàn toàn tin rằng nó giống nhau) –

+0

một trong những câu trả lời hoành tráng nhất tại SO áp dụng cho câu hỏi của bạn rất tốt: [Có thể truy cập bộ nhớ của biến cục bộ ngoài phạm vi của nó không?] (Http://stackoverflow.com/a/6445794/1025391) – moooeeeep

+5

MSVS được phép thực hiện bất kỳ tiện ích mở rộng ngôn ngữ nào mà bạn muốn. Tôi đồng ý với bạn rằng nó là lạ, mặc dù. –

Trả lời

5

Bạn nói đúng rằng "thời gian không được đảm bảo để thậm chí có bộ nhớ", theo nghĩa là tạm thời có thể không được lưu trữ trong bộ nhớ địa chỉ. Trên thực tế, các hàm rất thường được biên dịch cho các kiến ​​trúc RISC (ví dụ: ARM) sẽ trả về các giá trị trong sổ đăng ký sử dụng chung và sẽ mong đợi các đầu vào trong các thanh ghi đó.

MSVS, mã sản xuất cho kiến ​​trúc x86, có thể luôn luôn tạo các hàm trả về giá trị của chúng trên ngăn xếp. Do đó chúng được lưu trữ trong bộ nhớ địa chỉ và có một địa chỉ hợp lệ.

9

Một số thời gian nhất định có bộ nhớ. Bạn có thể làm một cái gì đó như thế này:

template<typename T> 
const T *get_temporary_address(const T &x) { 
    return &x; 
} 

int bar() { return 42; } 

int main() { 
    std::cout << (const void *)get_temporary_address(bar()) << std::endl; 
} 

Trong C++ 11, bạn có thể làm điều này với sự tham khảo rvalue không const quá:

template<typename T> 
T *get_temporary_address(T &&x) { 
    return &x; 
} 

int bar() { return 42; } 

int main() { 
    std::cout << (const void *)get_temporary_address(bar()) << std::endl; 
} 

Lưu ý, tất nhiên, rằng dereferencing con trỏ trong câu hỏi (bên ngoài số get_temporary_address chính nó) là một ý tưởng rất xấu; tạm thời chỉ sống đến cuối của biểu thức đầy đủ, và do đó, có một con trỏ để nó thoát khỏi biểu thức là hầu như luôn luôn là một công thức cho thảm họa.

Hơn nữa, lưu ý rằng không cần trình biên dịch để từ chối một chương trình không hợp lệ. Tiêu chuẩn C và C++ chỉ gọi cho chẩn đoán (ví dụ: cảnh báo lỗi hoặc), khi đó trình biên dịch có thể từ chối chương trình hoặc có thể biên dịch chương trình, với hành vi không xác định khi chạy. Nếu bạn muốn trình biên dịch của bạn từ chối nghiêm ngặt các chương trình tạo ra các chẩn đoán, hãy cấu hình nó để chuyển đổi các cảnh báo thành các lỗi.

+2

Lưu ý rằng bạn đang rất gần với hành vi không xác định. Tạm thời được giới thiệu bởi 'bar()' sẽ chỉ tồn tại cho đến khi kết thúc biểu thức đầy đủ, mà trong trường hợp này có thể không hoàn toàn rõ ràng: 'std :: cout.operator << ((const void *) get_temporary_address (thanh())) '. – Xeo

+0

@Xeo, Thật vậy. Tuy nhiên, chuyển đổi con trỏ thành một số nguyên mà không có dereferencing nó phải được xác định rõ, phải không? Giá trị chính xác sẽ không được chỉ định, tất nhiên. – bdonlan

+0

Tôi biết một trình biên dịch là không cần thiết, nhưng tôi mong đợi một dòng chính để hành động một cách hợp lý. Tôi đoán câu hỏi này là càng nhiều về guesstimating mức độ nhạy cảm của VS vì nó là về việc liệu thời gian có lưu trữ. –

4

Đối tượng tạm thời không có bộ nhớ. Đôi khi trình biên dịch tạo ra thời gian là tốt. Trong các trường hợp, những vật thể này sắp biến mất, tức là chúng không nên thu thập những thay đổi quan trọng một cách tình cờ. Do đó, bạn có thể giữ tạm thời chỉ thông qua tham chiếu rvalue hoặc tham chiếu const nhưng không thông qua tham chiếu không const. Lấy địa chỉ của một vật sắp biến mất cũng cảm thấy như một thứ nguy hiểm và do đó không được hỗ trợ.

Nếu bạn chắc chắn bạn muốn tham chiếu không phải const hoặc con trỏ từ đối tượng tạm thời, bạn có thể trả về từ một hàm thành viên tương ứng: bạn có thể gọi các hàm thành viên không phải là thời gian. Và bạn có thể trả lại this từ thành viên này. Tuy nhiên, lưu ý rằng hệ thống kiểu đang cố gắng giúp bạn. Khi bạn lừa nó, bạn tốt hơn biết rằng những gì bạn đang di chuyển là điều đúng.

+0

Tôi không muốn _use_ điều này; Tôi chỉ đang cố hợp lý hóa hành vi chẩn đoán này. Tôi đã nêu rõ trong câu hỏi rằng bạn không thể tuân theo địa chỉ tạm thời :) –

1

Temporaries không có bộ nhớ. Họ được cấp phát trên stack của người gọi (lưu ý: có thể là chủ đề của cuộc gọi hội nghị, nhưng tôi nghĩ họ ngăn xếp tất cả sử dụng của người gọi):

caller() 
{ 
callee1(Tmp()); 
callee2(Tmp()); 
} 

trình biên dịch sẽ phân bổ không gian cho kết quả Tmp() trên stack của caller. Bạn có thể lấy địa chỉ của vị trí bộ nhớ này - nó sẽ là một số địa chỉ trên ngăn xếp của caller.Trình biên dịch nào không đảm bảo rằng nó sẽ bảo tồn các giá trị tại địa chỉ ngăn xếp này sau khi trả về callee. Ví dụ, trình biên dịch có thể đặt có khác tạm thời, vv

EDIT: Tôi tin rằng, nó không được phép để loại bỏ mã như thế này:

T bar(); 
T * ptr = &bar(); 

vì nó sẽ rất có thể dẫn đến vấn đề này.

EDIT: đây là một little test:

#include <iostream> 

typedef long long int T64; 

T64 ** foo(T64 * fA) 
{ 

std::cout << "Address of tmp inside callee : " << &fA << std::endl; 

return (&fA); 
} 

int main(void) 
{ 
T64 lA = -1; 
T64 lB = -2; 
T64 lC = -3; 
T64 lD = -4; 

T64 ** ptr_tmp = foo(&lA); 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lA\t\t\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lA << std::endl << std::endl; 

foo(&lB); 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lB (compiler override)\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lB << std::endl 
    << std::endl; 

*ptr_tmp = &lC; 
std::cout << "Manual override" << std::endl << "**ptr_tmp = *(*ptr_tmp) = lC (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp 
    << " = " << lC << std::endl << std::endl; 

*ptr_tmp = &lD; 
std::cout << "Another attempt to manually override" << std::endl; 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lD (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lD << std::endl 
    << std::endl; 

return (0); 
} 

Chương trình đầu ra GCC:

Address of tmp inside callee : 0xbfe172f0 
**ptr_tmp = *(*ptr_tmp) = lA    **0xbfe172f0 = *(0xbfe17328) = -1 = -1 

Address of tmp inside callee : 0xbfe172f0 
**ptr_tmp = *(*ptr_tmp) = lB (compiler override) **0xbfe172f0 = *(0xbfe17320) = -2 = -2 

Manual override 
**ptr_tmp = *(*ptr_tmp) = lC (manual override)  **0xbfe172f0 = *(0xbfe17318) = -3 = -3 

Another attempt to manually override 
**ptr_tmp = *(*ptr_tmp) = lD (manual override)  **0xbfe172f0 = *(0x804a3a0) = -5221865215862754004 = -4 

đầu ra Chương trình VC++:

Address of tmp inside callee : 00000000001EFC10 
**ptr_tmp = *(*ptr_tmp) = lA       **00000000001EFC10 = *(000000013F42CB10) = -1 = -1 

Address of tmp inside callee : 00000000001EFC10 
**ptr_tmp = *(*ptr_tmp) = lB (compiler override)  **00000000001EFC10 = *(000000013F42CB10) = -2 = -2 

Manual override 
**ptr_tmp = *(*ptr_tmp) = lC (manual override)   **00000000001EFC10 = *(000000013F42CB10) = -3 = -3 

Another attempt to manually override 
**ptr_tmp = *(*ptr_tmp) = lD (manual override)   **00000000001EFC10 = *(000000013F42CB10) = 5356268064 = -4 

Thông báo, cả GCC và VC++ trữ trên stack trong số main biến địa phương ẩn cho thời gian và MIGHT âm thầm trở lại sử dụng chúng. Mọi thứ diễn ra bình thường, cho đến khi ghi đè thủ công cuối cùng: sau lần ghi đè thủ công cuối cùng, chúng tôi có cuộc gọi riêng biệt bổ sung tới std::cout. Nó sử dụng không gian ngăn xếp đến nơi chúng tôi vừa viết một cái gì đó, và kết quả là chúng tôi nhận được rác.

Tóm lại: cả GCC và VC++ phân bổ không gian cho thời gian trên ngăn xếp người gọi. Họ có thể có các chiến lược khác nhau về số lượng không gian để phân bổ, cách sử dụng lại không gian này (nó có thể phụ thuộc vào việc tối ưu hóa). Cả hai đều có thể tái sử dụng không gian này theo quyết định của họ và do đó, không an toàn để lấy địa chỉ tạm thời, vì chúng tôi có thể cố gắng truy cập thông qua địa chỉ này mà chúng tôi cho là vẫn có (nói, viết gì đó trực tiếp rồi thử để lấy nó), trong khi trình biên dịch có thể đã sử dụng lại nó và ghi đè giá trị của chúng tôi.

3

Như những người khác đã đề cập, tất cả chúng tôi đều đồng ý rằng thời gian có lưu trữ.

tại sao việc sử dụng địa chỉ tạm thời là bất hợp pháp?

Vì thời gian được phân bổ trên ngăn xếp, trình biên dịch được tự do sử dụng địa chỉ đó cho bất kỳ mục đích nào khác mà nó muốn.

int foo() 
{ 
int myvar=5; 
return &myvar; 
} 

int main() 
{ 
int *p=foo(); 
print("%d", *p); 
return 0; 
} 

Giả sử địa chỉ của 'myvar' là 0x1000. Chương trình này rất có thể sẽ in 99 mặc dù nó là bất hợp pháp để truy cập 0x1000 trong main(). Mặc dù, không nhất thiết phải tất cả thời gian.

Với một sự thay đổi nhỏ về chính trên():

int foo() 
{ 
int myvar=5; 
return &myvar; // address of myvar is 0x1000 
} 

int main() 
{ 
int *p=foo(); //illegal to access 0x1000 here 
print("%d", *p); 
fun(p); // passing *that address* to fun() 
return 0; 
} 

void fun(int *q) 
{ 
int a,b; //some variables 
print("%d", *q); 
} 

Các printf thứ hai là rất khó có khả năng in '5' như trình biên dịch có thể thậm chí phân bổ phần cùng của ngăn xếp (trong đó có 0x1000) cho vui() là tốt. Không có vấn đề liệu nó in '5' cho cả hai printfs HOẶC trong một trong số họ, nó là hoàn toàn một tác dụng phụ không chủ ý về cách bộ nhớ ngăn xếp đang được sử dụng/phân bổ. Đó là lý do tại sao việc truy cập địa chỉ không phải là số còn sống trong phạm vi đó là bất hợp pháp.

+0

Nhưng rõ ràng, vì VS không tuân thủ quy tắc, nên có thể thực hiện được điều này về mặt kỹ thuật. –

37

Thực ra, trong thiết kế ngôn ngữ gốc, được phép lấy địa chỉ tạm thời. Như bạn đã nhận thấy một cách chính xác, không có lý do kỹ thuật cho việc không cho phép điều này, và MSVC vẫn cho phép nó ngày hôm nay thông qua một phần mở rộng ngôn ngữ không chuẩn.

Lý do tại sao C++ làm cho nó bất hợp pháp là các tham chiếu ràng buộc tới các thời gian xung đột với một tính năng ngôn ngữ C++ khác được kế thừa từ C: Chuyển đổi loại ngầm định. xem xét:

void CalculateStuff(long& out_param) { 
    long result; 
    // [...] complicated calculations 
    out_param = result; 
} 

int stuff; 
CalculateStuff(stuff); //< this won't compile in ISO C++ 

CalculateStuff() có nghĩa vụ phải trả về kết quả của nó thông qua các tham số đầu ra. Nhưng điều thực sự xảy ra là: Hàm này chấp nhận long& nhưng được đưa ra một đối số kiểu int. Thông qua chuyển đổi loại ngầm định của C, rằng int giờ đây được chuyển đổi hoàn toàn thành biến số long, tạo tạm thời một tên chưa được đặt tên trong quá trình. Vì vậy, thay vì biến số stuff, hàm thực sự hoạt động trên một tên tạm thời chưa được đặt tên và tất cả các hiệu ứng phụ được áp dụng bởi hàm đó sẽ bị mất sau khi tạm thời bị hủy. Giá trị của biến stuff không bao giờ thay đổi. Các tài liệu tham khảo được giới thiệu tới C++ để cho phép quá tải toán tử, vì từ quan điểm của người gọi, chúng giống với cú pháp giá trị (ngược lại với các cuộc gọi con trỏ, yêu cầu rõ ràng & ở phía người gọi). Thật không may, nó chính xác là sự tương đương cú pháp dẫn đến những rắc rối khi kết hợp với chuyển đổi kiểu ngầm của C.

Vì Stroustrup muốn giữ cả hai tính năng (tham chiếu và C-khả năng tương thích), ông đã giới thiệu quy tắc mà chúng ta đều biết ngày nay: Các thời gian không được đặt tên chỉ liên kết với tham chiếu const. Với quy tắc bổ sung đó, mẫu trên không còn biên dịch nữa. Vì vấn đề chỉ xảy ra khi hàm áp dụng các hiệu ứng phụ cho tham số tham chiếu, nên vẫn an toàn để ràng buộc các thời gian không tên để tham chiếu const, do đó vẫn được cho phép.

toàn bộ câu chuyện này cũng được mô tả trong Chương 3.7 Thiết kế và Sự phát triển của C++:

Lý do để cho phép tham chiếu đến được khởi tạo bởi phi lvalues ​​là cho phép sự phân biệt giữa cuộc gọi-by-giá trị và call-by-reference là một chi tiết được chỉ định bởi hàm được gọi và không quan tâm đến người gọi. Đối với tài liệu tham khảo const, điều này là có thể; cho tài liệu tham khảo non-const nó không phải là. Đối với Release 2.0, định nghĩa của C++ đã được thay đổi để phản ánh điều này.

Tôi cũng mơ hồ nhớ đọc trong một bài báo đầu tiên phát hiện ra hành vi này, nhưng tôi không thể nhớ ngay bây giờ. Có lẽ ai đó có thể giúp tôi?

Các vấn đề liên quan