Một con trỏ có địa chỉ và kiểu phù hợp vẫn luôn là một con trỏ hợp lệ kể từ C ++ 17?


84

(Tham khảo câu hỏi và câu trả lời này .)

Trước tiêu chuẩn C ++ 17, câu sau được bao gồm trong [basic.compound] / 3 :

Nếu một đối tượng kiểu T nằm ở địa chỉ A, thì một con trỏ kiểu cv T * có giá trị là địa chỉ A được cho là trỏ tới đối tượng đó, bất kể giá trị đó được lấy như thế nào.

Nhưng kể từ C ++ 17, câu này đã bị loại bỏ .

Ví dụ: tôi tin rằng câu này đã làm cho mã ví dụ này được xác định và vì C ++ 17, đây là hành vi không xác định:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

Trước C ++ 17, p1+1giữ địa chỉ tới *p2và có kiểu phù hợp, *(p1+1)con trỏ tới cũng vậy *p2. Trong C ++, 17 p1+1là một con trỏ quá khứ , vì vậy nó không phải là một con trỏ tới đối tượng và tôi tin rằng nó không thể bỏ qua.

Việc giải thích sự sửa đổi tiêu chuẩn này có đúng không hay có những quy tắc khác bù đắp cho việc xóa câu trích dẫn?


Lưu ý: có các quy tắc mới / cập nhật về xuất xứ của con trỏ trong [basic.stc.dynamic.safety] và [Prac.dynamic.safety]
MM

@MM Điều đó chỉ quan trọng đối với các triển khai có độ an toàn con trỏ nghiêm ngặt, là một tập hợp trống (nằm trong lỗi thử nghiệm).
TC

4
Tuyên bố được trích dẫn chưa bao giờ đúng trong thực tế. Đã cho int a, b = 0;, bạn không thể làm *(&a + 1) = 1;ngay cả khi bạn đã kiểm tra &a + 1 == &b. Nếu bạn có thể nhận được một con trỏ hợp lệ đến một đối tượng chỉ bằng cách đoán địa chỉ của nó, thì ngay cả việc lưu trữ các biến cục bộ trong các thanh ghi cũng trở thành vấn đề.
TC

@TC 1) Trình biên dịch nào đặt var trong reg sau khi bạn đã lấy địa chỉ của nó? 2) Làm thế nào để bạn đoán một địa chỉ một cách chính xác mà không cần đo nó?
tò mò

@curiousguy Chính xác đó là lý do tại sao chỉ cần truyền một số thu được bằng các phương tiện khác (ví dụ: đoán) đến địa chỉ nơi một đối tượng xảy ra là có vấn đề: Nó đặt bí danh cho đối tượng đó nhưng trình biên dịch không biết về nó. Ngược lại, nếu bạn lấy địa chỉ của đối tượng như bạn nói: trình biên dịch được cảnh báo và đồng bộ hóa tương ứng.
Peter - Phục hồi Monica

Câu trả lời:


45

Việc giải thích sự sửa đổi tiêu chuẩn này có đúng không hay có những quy tắc khác bù đắp cho việc xóa câu trích dẫn này?

Vâng, cách giải thích này là đúng. Một con trỏ quá cuối không chỉ đơn giản là có thể chuyển đổi thành một giá trị con trỏ khác mà tình cờ trỏ đến địa chỉ đó.

Mới [basic.compound] / 3 nói:

Mọi giá trị của kiểu con trỏ là một trong những giá trị sau:
(3.1) một con trỏ tới một đối tượng hoặc hàm (con trỏ được cho là trỏ đến đối tượng hoặc hàm), hoặc
(3.2) một con trỏ qua phần cuối của một đối tượng ([expr .Thêm hoặc

Đó là những thứ loại trừ lẫn nhau. p1+1là một con trỏ qua cuối, không phải là một con trỏ tới một đối tượng. p1+1chỉ đến một giả thuyết x[1]của một mảng kích thước-1 tại p1, không phải p2. Hai đối tượng đó không thể hoán đổi con trỏ lẫn nhau.

Chúng tôi cũng có lưu ý không theo quy chuẩn:

[Lưu ý: Một con trỏ qua phần cuối của một đối tượng ([expr.add]) không được coi là trỏ đến một đối tượng không liên quan cùng loại đối tượng có thể nằm tại địa chỉ đó. [...]

làm rõ ý định.


Như TC đã chỉ ra trong nhiều nhận xét ( đặc biệt là nhận xét này ), đây thực sự là một trường hợp đặc biệt của vấn đề đi kèm với việc cố gắng triển khai std::vector- nghĩa là [v.data(), v.data() + v.size())cần phải là một phạm vi hợp lệ và chưa vectortạo đối tượng mảng, vì vậy chỉ số học con trỏ xác định sẽ đi từ bất kỳ đối tượng nhất định nào trong vectơ đến quá khứ-cuối của mảng một kích thước giả định của nó. Để có thêm tài nguyên, hãy xem CWG 2182 , cuộc thảo luận đầu tiên này và hai bản sửa đổi của một bài báo về chủ đề: P0593R0P0593R1 (cụ thể là phần 1.3).


3
Ví dụ này về cơ bản là một trường hợp đặc biệt của " vectorvấn đề khả năng triển khai" đã biết. +1.
TC

2
@Oliv Trường hợp chung đã tồn tại kể từ C ++ 03. Nguyên nhân gốc rễ là số học con trỏ không hoạt động như mong đợi vì bạn không có đối tượng mảng.
TC

1
@TC Tôi tin rằng vấn đề duy nhất đến từ hạn chế về số học con trỏ. Không phải là xóa câu này thêm một vấn đề mới? Có phải ví dụ mã cũng UB trong trước C ++ 17 không?
Oliv

1
@Oliv Nếu số học của con trỏ được cố định, thì của bạn p1+1sẽ không tạo ra con trỏ quá khứ và toàn bộ cuộc thảo luận về con trỏ quá khứ cuối cùng là tranh luận. Trường hợp đặc biệt hai phần tử cụ thể của bạn có thể không phải là UB trước 17, nhưng nó cũng không thú vị lắm.
TC

5
@TC Bạn có thể chỉ cho tôi một nơi nào đó mà tôi có thể đọc về "vấn đề khả năng triển khai vectơ" này không?
SirGuy

8

Trong ví dụ của bạn, *(p1 + 1) = 10;nên là UB, bởi vì nó là một phần cuối của mảng có kích thước 1. Nhưng chúng ta đang ở trong một trường hợp rất đặc biệt ở đây, bởi vì mảng được xây dựng động trong một mảng char lớn hơn.

Tạo đối tượng động được mô tả trong 4.5 Mô hình đối tượng C ++ [intro.object] , §3 của bản nháp n4659 của tiêu chuẩn C ++:

3 Nếu một đối tượng hoàn chỉnh được tạo (8.3.4) trong bộ nhớ được liên kết với một đối tượng e khác thuộc loại “mảng của N unsigned char” hoặc thuộc loại “mảng của N std :: byte” (21.2.1), mảng đó cung cấp bộ nhớ đối với đối tượng được tạo nếu:
(3.1) - thời gian tồn tại của e đã bắt đầu và chưa kết thúc, và
(3.2) - bộ nhớ cho đối tượng mới hoàn toàn phù hợp với e, và
(3.3) - không có đối tượng mảng nào nhỏ hơn đáp ứng các những ràng buộc.

3.3 có vẻ khá rõ ràng, nhưng các ví dụ dưới đây làm cho ý định rõ ràng hơn:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Vì vậy, trong ví dụ, buffermảng cung cấp bộ nhớ cho cả *p1*p2.

Các đoạn văn sau chứng minh rằng đối tượng hoàn chỉnh cho cả hai *p1*p2buffer:

4 Một đối tượng a được lồng trong một đối tượng khác b nếu:
(4.1) - a là một đối tượng chính của b, hoặc
(4.2) - b cung cấp bộ nhớ cho a, hoặc
(4.3) - tồn tại một đối tượng c trong đó a được lồng trong c , và c được lồng trong b.

5 Với mọi đối tượng x, có một đối tượng nào đó được gọi là đối tượng hoàn chỉnh của x, được xác định như sau:
(5.1) - Nếu x là đối tượng hoàn chỉnh thì đối tượng hoàn chỉnh của x là chính nó.
(5.2) - Nếu không, đối tượng hoàn chỉnh của x là đối tượng hoàn chỉnh của đối tượng (duy nhất) chứa x.

Khi điều này được thiết lập, phần liên quan khác của bản nháp n4659 cho C ++ 17 là [basic.coumpound] §3 (nhấn mạnh của tôi):

3 ... Mọi giá trị của kiểu con trỏ là một trong những giá trị sau:
(3.1) - một con trỏ tới một đối tượng hoặc hàm (con trỏ được cho là trỏ đến đối tượng hoặc hàm), hoặc
(3.2) - một con trỏ ở cuối của một đối tượng (8.7), hoặc
(3.3) - giá trị con trỏ null (7.11) cho kiểu đó, hoặc
(3.4) - giá trị con trỏ không hợp lệ.

Một giá trị của kiểu con trỏ là một con trỏ đến hoặc qua phần cuối của một đối tượng đại diện cho địa chỉ của byte đầu tiên trong bộ nhớ (4.4) bị chiếm bởi đối tượng hoặc byte đầu tiên trong bộ nhớ sau khi kết thúc bộ nhớ mà đối tượng chiếm giữ , tương ứng. [Lưu ý: Một con trỏ qua phần cuối của một đối tượng (8.7) không được coi là trỏ đến một đối tượng không liên quanđối tượng của loại đối tượng có thể được đặt tại địa chỉ đó. Một giá trị con trỏ trở nên không hợp lệ khi bộ nhớ mà nó biểu thị đạt đến cuối thời hạn lưu trữ của nó; xem 6.7. —End note] Đối với mục đích số học con trỏ (8.7) và so sánh (8.9, 8.10), một con trỏ qua phần cuối của phần tử cuối cùng của mảng x gồm n phần tử được coi là tương đương với một con trỏ đến phần tử giả định x [ n]. Biểu diễn giá trị của các kiểu con trỏ được xác định bởi việc triển khai. Con trỏ tới các loại tương thích với bố cục phải có cùng giá trị biểu diễn và các yêu cầu liên kết (6.11) ...

Các lưu ý Một con trỏ qua cuối cùng ... không áp dụng ở đây vì các đối tượng được trỏ đến bởi p1p2và không liên quan , nhưng được lồng vào đối tượng hoàn toàn giống nhau, vì vậy arithmetics con trỏ có ý nghĩa bên trong đối tượng cung cấp lưu trữ: p2 - p1được định nghĩa và là (&buffer[sizeof(int)] - buffer]) / sizeof(int)đó là 1.

Vì vậy, p1 + 1 một con trỏ tới *p2, và *(p1 + 1) = 10;có hành vi được xác định và thiết lập giá trị của *p2.


Tôi cũng đã đọc phụ lục C4 về khả năng tương thích giữa C ++ 14 và các tiêu chuẩn hiện tại (C ++ 17). Loại bỏ khả năng sử dụng số học con trỏ giữa các đối tượng được tạo động trong một mảng ký tự đơn lẻ sẽ là một thay đổi quan trọng mà IMHO nên được trích dẫn ở đó, vì nó là một tính năng thường được sử dụng. Vì không có gì về nó tồn tại trong các trang tương thích, tôi nghĩ rằng nó xác nhận rằng nó không phải là mục đích của tiêu chuẩn để cấm nó.

Đặc biệt, nó sẽ đánh bại cấu trúc động phổ biến của một mảng đối tượng từ một lớp không có hàm tạo mặc định:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr sau đó có thể được sử dụng như một con trỏ đến phần tử đầu tiên của một mảng ...


Aha, vậy là thế giới đã không trở nên điên cuồng. 1
người kể chuyện - Unslander Monica

@StoryTeller: Tôi cũng hy vọng. Ngoài ra không có một từ nào về nó trong phần tương thích. Nhưng có vẻ như ý kiến ​​ngược lại có nhiều tiếng tăm hơn ở đây ...
Serge Ballesta

2
Bạn đang nắm bắt một từ duy nhất, "không liên quan", trong một ghi chú không theo quy tắc và cho nó một ý nghĩa mà nó không thể chịu đựng được, trái ngược với các quy tắc quy chuẩn trong số học con trỏ điều chỉnh [expr.add]. Không có gì trong Phụ lục C vì số học con trỏ trường hợp chung chưa bao giờ hoạt động trong bất kỳ tiêu chuẩn nào. Không có gì để phá vỡ.
TC

3
@TC: Google rất vô ích trong việc tìm kiếm bất kỳ thông tin nào về "vấn đề khả năng triển khai vectơ" này, bạn có thể giúp gì không?
Matthieu M.

6
@MatthieuM. Xem vấn đề cốt lõi 2182 , chuỗi thảo luận std này , P0593R0P0593R1 (đặc biệt là phần 1.3) . Vấn đề cơ bản là vectorkhông (và không thể) tạo một đối tượng mảng, nhưng có một giao diện cho phép người dùng lấy một con trỏ hỗ trợ số học con trỏ (chỉ được định nghĩa cho các con trỏ vào đối tượng mảng).
TC

1

Để mở rộng các câu trả lời được đưa ra, đây là một ví dụ về những gì tôi tin rằng cách diễn đạt đã sửa đổi loại trừ:

Cảnh báo: Hành vi không xác định

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

Vì những lý do phụ thuộc hoàn toàn vào việc triển khai (và mong manh), đầu ra có thể có của chương trình này là:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Kết quả đó cho thấy rằng hai mảng (trong trường hợp đó) tình cờ được lưu trữ trong bộ nhớ sao cho 'một quá cuối' Axảy ra để giữ giá trị địa chỉ của phần tử đầu tiên của B.

Đặc điểm kỹ thuật sửa đổi đảm bảo rằng bất kể A+1không bao giờ là một con trỏ hợp lệ đến B. Cụm từ cũ 'bất kể giá trị thu được như thế nào' nói rằng nếu 'A + 1' chỉ đến 'B [0]' thì đó là một con trỏ hợp lệ tới 'B [0]'. Điều đó không thể tốt và chắc chắn không bao giờ là ý định.


Điều này cũng có hiệu quả loại bỏ việc sử dụng một mảng trống ở cuối cấu trúc để một lớp dẫn xuất hoặc trình cấp phát tùy chỉnh mới có thể chỉ định một mảng có kích thước tùy chỉnh? Có lẽ vấn đề mới là với "bất kể cách nào" - có một số cách hợp lệ và một số cách nguy hiểm?
Gem Taylor

@Persixty Vì vậy, giá trị của một đối tượng con trỏ được xác định bởi các byte của đối tượng, và không có gì khác. Vậy hai vật có cùng trạng thái thì hướng đến cùng một vật. Nếu một cái hợp lệ, cái kia cũng vậy. Vì vậy, trên các kiến ​​trúc thông thường, trong đó một giá trị con trỏ được biểu diễn dưới dạng số, hai con trỏ có giá trị bằng nhau trỏ đến các đối tượng giống nhau và một đầu là các đối tượng khác.
curiousguy

@Persixty Ngoài ra, kiểu tầm thường có nghĩa là bạn có thể liệt kê các giá trị có thể có của một kiểu. Về cơ bản, bất kỳ trình biên dịch hiện đại nào trong bất kỳ chế độ tối ưu hóa nào (ngay cả -O0trên một số trình biên dịch) đều không coi con trỏ là loại tầm thường. Những người biên dịch không coi trọng các yêu cầu của std, và những người viết std cũng vậy, những người mơ về một ngôn ngữ khác và tạo ra mọi loại phát minh mâu thuẫn trực tiếp với các nguyên tắc cơ bản. Rõ ràng là người dùng bị nhầm lẫn và đôi khi bị đối xử tệ khi họ phàn nàn về các lỗi trình biên dịch.
curiousguy

Lưu ý không quy chuẩn trong câu hỏi muốn chúng ta nghĩ về 'một quá khứ-kết thúc' như là không chỉ đến bất cứ điều gì. Cả hai chúng ta đều biết trong thực tế có thể đang chỉ vào một cái gì đó và trong thực tế, có thể bỏ qua nó. Nhưng đó (theo tiêu chuẩn) không phải là một chương trình hợp lệ. Chúng ta có thể tưởng tượng một triển khai biết một con trỏ được lấy bằng số học-quá khứ-cuối và tạo ra một ngoại lệ nếu được tham chiếu đến. Trong khi tôi biết về nền tảng biết làm điều đó. Tôi nghĩ rằng tiêu chuẩn không muốn loại trừ nó.
Persixty

@curiousguy Ngoài ra, tôi không chắc bạn muốn nói gì khi liệt kê các giá trị có thể. Đó không phải là một tính năng bắt buộc của một kiểu tầm thường như được định nghĩa bởi C ++.
Persixty
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.