Có hợp pháp để lập chỉ mục vào một cấu trúc không?


104

Bất kể mã 'xấu' như thế nào và giả sử rằng căn chỉnh, v.v. không phải là vấn đề trên trình biên dịch / nền tảng, thì đây có phải là hành vi không xác định hoặc bị hỏng không?

Nếu tôi có một cấu trúc như thế này: -

struct data
{
    int a, b, c;
};

struct data thing;

Là nó hợp pháp để truy cập a, bcnhư (&thing.a)[0], (&thing.a)[1](&thing.a)[2]?

Trong mọi trường hợp, trên mọi trình biên dịch và nền tảng tôi đã thử, với mọi cài đặt tôi đã thử, nó đều 'hoạt động'. Tôi chỉ lo lắng rằng trình biên dịch có thể không nhận ra rằng bđiều [1] là cùng một thứ và các kho lưu trữ cho 'b' có thể được đưa vào một thanh ghi và điều [1] đọc sai giá trị từ bộ nhớ (ví dụ). Mặc dù vậy, trong mọi trường hợp, tôi đã thử nó đã làm đúng. (Tôi nhận ra tất nhiên điều đó không chứng minh được nhiều)

Đây không phải là mã của tôi; đó là mã mà tôi phải làm việc, tôi quan tâm đến việc đây là mã xấu hay mã bị hỏng vì sự khác biệt ảnh hưởng đến ưu tiên của tôi trong việc thay đổi nó rất nhiều :)

Được gắn thẻ C và C ++. Tôi chủ yếu quan tâm đến C ++ nhưng cũng có C nếu nó khác, chỉ vì quan tâm.


51
Không, nó không phải là "hợp pháp". Đó là hành vi không xác định.
Sam Varshavchik

10
Nó hoạt động cho bạn trong trường hợp rất đơn giản này vì trình biên dịch không thêm bất kỳ khoảng đệm nào giữa các thành viên. Hãy thử với các cấu trúc sử dụng các loại có kích thước khác nhau và sẽ sụp đổ.
Một số lập trình viên dude

7
Đào lại quá khứ - UB từng có biệt danh là quỷ mũi .
Adrian Colomitchi

21
Thật tuyệt, ở đây tôi vấp phải vì tôi theo dõi thẻ C, đọc câu hỏi, sau đó viết câu trả lời chỉ áp dụng cho C, vì tôi không nhìn thấy thẻ C ++. C và C ++ rất khác nhau ở đây! C cho phép loại punning với các hợp nhất, C ++ thì không.
Lundin

7
Nếu bạn cần truy cập các phần tử dưới dạng một mảng, hãy xác định chúng dưới dạng một mảng. Nếu họ cần có các tên khác nhau, hãy sử dụng các tên đó. Cố gắng lấy chiếc bánh của bạn và ăn nó cuối cùng sẽ dẫn đến chứng khó tiêu - có thể là vào thời điểm bất tiện nhất trong tưởng tượng. (Tôi nghĩ rằng chỉ số 0 là hợp pháp trong C; chỉ số 1 hoặc 2 không phải là có bối cảnh trong đó một yếu tố duy nhất được coi là một mảng có kích thước 1..)
Jonathan Leffler

Câu trả lời:


73

Nó là bất hợp pháp 1 . Đó là một hành vi không xác định trong C ++.

Bạn đang lấy các thành viên theo kiểu mảng, nhưng đây là những gì tiêu chuẩn C ++ nói (tôi nhấn mạnh):

[dcl.array / 1] : ... Một đối tượng của kiểu mảng chứa một liên tục kế nhau phân bổ bộ không rỗng của N subobjects kiểu T ...

Tuy nhiên, đối với các thành viên, không có yêu cầu liền kề như vậy :

[class.mem / 17] : ...; Các yêu cầu liên kết triển khai có thể khiến hai thành viên liền kề không được phân bổ ngay sau nhau ...

Mặc dù hai dấu ngoặc kép trên đủ để gợi ý lý do tại sao việc lập chỉ mục thành một structnhư bạn đã làm không phải là một hành vi được xác định bởi tiêu chuẩn C ++, chúng ta hãy chọn một ví dụ: nhìn vào biểu thức (&thing.a)[2]- Về toán tử chỉ số:

[expr.post//expr.sub/1] : Biểu thức hậu tố theo sau là biểu thức trong dấu ngoặc vuông là biểu thức hậu tố. Một trong các biểu thức phải là giá trị kiểu “mảng T” hoặc giá trị p của kiểu “con trỏ tới T” và biểu thức còn lại phải là giá trị p của kiểu liệt kê hoặc tích phân chưa được đánh dấu. Kết quả là loại "T". Kiểu “T” phải là một kiểu đối tượng hoàn toàn được xác định.66 Biểu thức E1[E2]giống hệt (theo định nghĩa) với((E1)+(E2))

Đi sâu vào văn bản in đậm của trích dẫn trên: liên quan đến việc thêm một kiểu tích phân vào một kiểu con trỏ (lưu ý nhấn mạnh ở đây) ..

[expr.add / 4] : Khi một biểu thức có kiểu tích phân được thêm vào hoặc trừ khỏi một con trỏ, kết quả có kiểu của toán hạng con trỏ. Nếu biểu thứcPtrỏ đến phần tửx[i]của một đối tượng xmảng có n phần tử, các biểu thứcP + JJ + P(trong đóJcó giá trịj) trỏ đến phần tử (có thể là giả thuyết)x[i + j] if0 ≤ i + j ≤ n; nếu không , hành vi là không xác định. ...

Lưu ý yêu cầu về mảng đối với mệnh đề if ; khác khác trong trích dẫn trên. Biểu thức (&thing.a)[2]rõ ràng không đủ điều kiện cho mệnh đề if ; Do đó, Hành vi không xác định.


Một lưu ý nhỏ: Mặc dù tôi đã thử nghiệm rộng rãi mã và các biến thể của nó trên các trình biên dịch khác nhau và họ không giới thiệu bất kỳ phần đệm nào ở đây, (nó hoạt động ); từ quan điểm bảo trì, mã cực kỳ mong manh. bạn vẫn nên khẳng định rằng việc triển khai đã phân bổ các thành viên liên tục trước khi thực hiện việc này. Và ở trong giới hạn :-). Nhưng hành vi vẫn chưa được xác định của nó ....

Một số cách giải quyết khả thi (với hành vi xác định) đã được cung cấp bởi các câu trả lời khác.



Như đã chỉ ra đúng trong các nhận xét, [basic.lval / 8] , trong bản chỉnh sửa trước của tôi không áp dụng. Cảm ơn @ 2501 và @MM

1 : Xem câu trả lời của @ Barry cho câu hỏi này để biết một trường hợp pháp lý duy nhất mà bạn có thể truy cập thing.athành viên của cấu trúc thông qua parttern này.


1
@jcoder Nó được định nghĩa trong class.mem . Xem đoạn cuối để biết văn bản thực tế.
NathanOliver

4
Điều chỉnh nghiêm ngặt không liên quan ở đây. Kiểu int được chứa trong kiểu tổng hợp và kiểu này có thể là bí danh int. - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@ Những người phản đối, quan tâm để bình luận? - và để cải thiện hoặc chỉ ra câu trả lời này sai ở đâu?
WhiZTiM

4
Bí danh nghiêm ngặt không liên quan đến điều này. Phần đệm không phải là một phần của giá trị được lưu trữ của một đối tượng. Ngoài ra, câu trả lời này không giải quyết được trường hợp phổ biến nhất: điều gì xảy ra khi không có phần đệm. Thực sự khuyên bạn nên xóa câu trả lời này.
MM

1
Làm xong! Tôi đã xóa đoạn nói về dấu hiệu chặt chẽ.
WhiZTiM

48

Không. Trong C, đây là hành vi không xác định ngay cả khi không có phần đệm.

Điều gây ra hành vi không xác định là quyền truy cập ngoài giới hạn 1 . Khi bạn có một đại lượng vô hướng (các thành viên a, b, c trong cấu trúc) và cố gắng sử dụng nó như một mảng 2 để truy cập phần tử giả định tiếp theo, bạn gây ra hành vi không xác định, ngay cả khi có một đối tượng khác cùng loại tại địa chỉ đó.

Tuy nhiên, bạn có thể sử dụng địa chỉ của đối tượng struct và tính toán phần bù vào một thành viên cụ thể:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

Điều này phải được thực hiện cho từng thành viên riêng lẻ, nhưng có thể được đưa vào một hàm tương tự như quyền truy cập mảng.


1 (Trích dẫn từ: ISO / IEC 9899: 201x 6.5.6 Toán tử cộng 8)
Nếu kết quả trỏ qua phần tử cuối cùng của đối tượng mảng, thì nó sẽ không được sử dụng làm toán hạng của toán tử một ngôi * được đánh giá.

2 (Trích dẫn từ: ISO / IEC 9899: 201x 6.5.6 Toán tử cộng 7)
Đối với mục đích của các toán tử này, một con trỏ đến một đối tượng không phải là một phần tử của một mảng hoạt động giống như một con trỏ đến phần tử đầu tiên của một mảng có độ dài một với kiểu của đối tượng là kiểu phần tử của nó.


3
Xin lưu ý rằng điều này chỉ hoạt động nếu lớp là kiểu bố cục tiêu chuẩn. Nếu không nó vẫn là UB.
NathanOliver

@NathanOliver Tôi nên đề cập rằng câu trả lời của tôi chỉ áp dụng cho C. Đã chỉnh sửa. Đây là một trong những vấn đề của câu hỏi ngôn ngữ thẻ kép như vậy.
2501

Cảm ơn và đó là lý do tại sao tôi hỏi riêng về C ++ và C vì thật thú vị khi biết được sự khác biệt
jcoder 14/11/16

@NathanOliver Địa chỉ của thành viên đầu tiên được đảm bảo trùng với địa chỉ của lớp C ++ nếu nó được bố trí chuẩn. Tuy nhiên, điều đó không đảm bảo rằng truy cập được xác định rõ và cũng không ngụ ý rằng các truy cập đó trên các lớp khác là không xác định.
Potatoswatter vào

bạn sẽ nói rằng điều đó char* p = ( char* )&thing.a + offsetof( thing , b );dẫn đến hành vi không xác định?
MM

43

Trong C ++ nếu bạn thực sự cần - hãy tạo toán tử []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

nó không chỉ đảm bảo hoạt động mà cách sử dụng đơn giản hơn, bạn không cần phải viết biểu thức không đọc được (&thing.a)[0]

Lưu ý: câu trả lời này được đưa ra trong giả định rằng bạn đã có cấu trúc với các trường và bạn cần thêm quyền truy cập thông qua chỉ mục. Nếu tốc độ là một vấn đề và bạn có thể thay đổi cấu trúc, điều này có thể hiệu quả hơn:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Giải pháp này sẽ thay đổi kích thước cấu trúc để bạn cũng có thể sử dụng các phương pháp:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
Tôi muốn thấy việc tháo gỡ này, so với việc tháo gỡ một chương trình C bằng cách sử dụng kiểu punning. Nhưng, nhưng ... C ++ cũng nhanh như C ... phải không? Đúng?
Lundin

6
@Lundin nếu bạn quan tâm đến tốc độ của quá trình xây dựng này thì dữ liệu nên được tổ chức như một mảng ngay từ đầu, không phải là các trường riêng biệt.
Slava

2
@Lundin theo cả bạn đều có nghĩa là Hành vi không đọc được và Không xác định? Không, cám ơn.
Slava

1
Nạp chồng toán tử @Lundin là một tính năng cú pháp thời gian biên dịch không gây ra bất kỳ chi phí nào so với các hàm thông thường. Hãy xem godbolt.org/g/vqhREz để xem trình biên dịch thực sự làm gì khi nó biên dịch mã C ++ và C. Thật đáng kinh ngạc về những gì họ làm và những gì người ta mong đợi họ làm. Cá nhân tôi thích độ an toàn và biểu cảm kiểu chữ tốt hơn của C ++ hơn C hàng triệu lần. Và nó hoạt động mọi lúc mà không cần dựa vào các giả định về lớp đệm.
Jens

2
Những tham chiếu đó ít nhất sẽ tăng gấp đôi kích thước của thứ. Chỉ cần làm thing.a().
TC

14

Đối với c ++: Nếu bạn cần truy cập một thành viên mà không biết tên của nó, bạn có thể sử dụng một con trỏ đến biến thành viên.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
Điều này đang sử dụng các phương tiện ngôn ngữ, và kết quả là được xác định rõ ràng và như tôi cho là hiệu quả. Câu trả lời tốt nhất.
Peter - Phục hồi Monica

2
Giả sử hiệu quả? Tôi giả định ngược lại. Nhìn vào mã đã tạo.
JDługosz

1
@ JDługosz, bạn nói khá đúng. Tham gia một peek tại tạo lắp ráp, có vẻ như gcc 6.2 tạo ra mã tương đương để sử dụng offsetofftrong C.
người kể chuyện - Unslander Monica

3
bạn cũng có thể cải thiện mọi thứ bằng cách tạo arr constexpr. Thao tác này sẽ tạo một bảng tra cứu cố định duy nhất trong phần dữ liệu chứ không phải tạo một cách nhanh chóng.
Tim

10

Trong ISO C99 / C11, đánh lừa kiểu dựa trên liên hợp là hợp pháp, vì vậy bạn có thể sử dụng điều đó thay vì lập chỉ mục con trỏ đến không phải mảng (xem nhiều câu trả lời khác).

ISO C ++ không cho phép xử lý kiểu dựa trên liên hợp. GNU C ++, như một phần mở rộng , và tôi nghĩ rằng một số trình biên dịch khác không hỗ trợ phần mở rộng GNU nói chung có hỗ trợ kiểu kết hợp-punning. Nhưng điều đó không giúp bạn viết mã di động nghiêm ngặt.

Với các phiên bản gcc và clang hiện tại, việc viết một hàm thành viên C ++ bằng cách sử dụng a switch(idx)để chọn một thành viên sẽ tối ưu hóa cho các chỉ số không đổi thời gian biên dịch, nhưng sẽ tạo ra nhiều nhánh khủng khiếp cho các chỉ số thời gian chạy. Không có gì sai với switch()điều này; đây chỉ đơn giản là một lỗi tối ưu hóa bị bỏ sót trong các trình biên dịch hiện tại. Họ có thể biên dịch hàm switch () của Slava một cách hiệu quả.


Giải pháp / cách giải quyết này là thực hiện theo cách khác: cung cấp cho lớp / struct của bạn một thành viên mảng và viết các hàm truy cập để đính kèm tên cho các phần tử cụ thể.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

Chúng ta có thể xem đầu ra asm cho các trường hợp sử dụng khác nhau, trên trình khám phá trình biên dịch Godbolt . Đây là các hàm System V hoàn chỉnh của x86-64, với lệnh RET ở cuối bị bỏ qua để hiển thị tốt hơn những gì bạn nhận được khi chúng nội dòng. ARM / MIPS / bất cứ thứ gì sẽ tương tự.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

Để so sánh, câu trả lời của @ Slava sử dụng a switch()cho C ++ làm cho asm giống như thế này cho một chỉ mục biến thời gian chạy. (Mã trong liên kết Godbolt trước).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Điều này rõ ràng là khủng khiếp, so với phiên bản xảo quyệt kiểu liên minh dựa trên C (hoặc GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

@MM: điểm tốt. Nó giống như một câu trả lời cho nhiều bình luận khác nhau và một sự thay thế cho câu trả lời của Slava. Tôi đã diễn đạt lại phần mở đầu, vì vậy ít nhất nó cũng bắt đầu như một câu trả lời cho câu hỏi ban đầu. Cảm ơn vì đã chỉ ra điều đó.
Peter Cordes

Mặc dù punning kiểu dựa trên union dường như hoạt động trong gcc và clang khi sử dụng []toán tử trực tiếp trên một thành viên union, nhưng Tiêu chuẩn định nghĩa array[index]là tương đương với *((array)+(index))và cả gcc và clang đều không nhận ra một cách đáng tin cậy rằng một quyền truy cập *((someUnion.array)+(index))là một quyền truy cập vào someUnion. Lời giải thích duy nhất mà tôi có thể thấy là điều đó someUnion.array[index]cũng *((someUnion.array)+(index))không được Tiêu chuẩn xác định, mà chỉ đơn thuần là một phần mở rộng phổ biến và gcc / clang đã chọn không hỗ trợ phần thứ hai nhưng dường như hỗ trợ phần đầu tiên, ít nhất là hiện tại.
supercat

9

Trong C ++, đây chủ yếu là hành vi không xác định (nó phụ thuộc vào chỉ mục nào).

Từ [expr.unary.op]:

Đối với mục đích số học con trỏ (5.7) và so sánh (5.9, 5.10), một đối tượng không phải là phần tử mảng có địa chỉ được lấy theo cách này được coi là thuộc về mảng có một phần tử kiểu T.

Do đó, biểu thức &thing.ađược coi là tham chiếu đến một mảng của một int.

Từ [expr.sub]:

Biểu thức E1[E2]giống hệt (theo định nghĩa) với*((E1)+(E2))

Và từ [expr.add]:

Khi một biểu thức có kiểu tích phân được thêm vào hoặc trừ khỏi con trỏ, kết quả có kiểu toán hạng con trỏ. Nếu biểu thức Ptrỏ đến phần tử x[i]của một đối tượng mảng xncác phần tử, các biểu thức P + JJ + P (trong đó Jcó giá trị j) trỏ đến phần tử (có thể là giả thuyết) x[i + j]if 0 <= i + j <= n; nếu không, hành vi là không xác định.

(&thing.a)[0]được định dạng hoàn hảo vì &thing.ađược coi là một mảng có kích thước 1 và chúng tôi đang lấy chỉ mục đầu tiên đó. Đó là một chỉ số được phép lấy.

(&thing.a)[2] vi phạm điều kiện tiên quyết rằng 0 <= i + j <= n , vì chúng ta có i == 0, j == 2, n == 1. Chỉ cần xây dựng con trỏ &thing.a + 2là hành vi không xác định.

(&thing.a)[1]là trường hợp thú vị. Nó không thực sự vi phạm bất cứ điều gì trong [expr.add]. Chúng tôi được phép đưa một con trỏ qua phần cuối của mảng - đây sẽ là con trỏ. Ở đây, chúng ta chuyển sang một ghi chú trong [basic.compound]:

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ớ (1.7) bị chiếm bởi đối tượng53 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 (5.7) không được coi là trỏ đến một đối tượng không liên quan thuộc loại đối tượng có thể nằm ở địa chỉ đó.

Do đó, lấy con trỏ &thing.a + 1là hành vi được xác định, nhưng tham chiếu đến nó là không xác định vì nó không trỏ đến bất cứ thứ gì.


Đánh giá (& thing.a) + 1 chỉ là về mặt pháp lý vì một con trỏ ở cuối mảng là hợp pháp; đọc hoặc ghi dữ liệu được lưu trữ ở đó là hành vi không xác định, so sánh với & thing.b với <,>, <=,> = là hành vi không xác định. (& thing.a) + 2 là hoàn toàn bất hợp pháp.
gnasher729,

@ gnasher729 Vâng, đáng để làm rõ câu trả lời hơn.
Barry

Đây (&thing.a + 1)là một trường hợp thú vị mà tôi đã thất bại. +1! ... Chỉ tò mò, bạn có ở trong ủy ban ISO C ++ không?
WhiZTiM

Đây cũng là một trường hợp rất quan trọng vì nếu không thì mọi vòng lặp sử dụng con trỏ như một khoảng nửa mở sẽ là UB.
Jens

Về trích dẫn tiêu chuẩn cuối cùng. C ++ phải được chỉ định tốt hơn C ở đây.
2501

8

Đây là hành vi không xác định.

Có rất nhiều quy tắc trong C ++ cố gắng cung cấp cho trình biên dịch một số hy vọng hiểu được những gì bạn đang làm, vì vậy nó có thể suy luận về nó và tối ưu hóa nó.

Có các quy tắc về răng cưa (truy cập dữ liệu thông qua hai loại con trỏ khác nhau), giới hạn mảng, v.v.

Khi bạn có một biến x, thực tế là nó không phải là thành viên của một mảng có nghĩa là trình biên dịch có thể cho rằng không có []quyền truy cập mảng dựa trên nào có thể sửa đổi nó. Vì vậy, nó không phải liên tục tải lại dữ liệu từ bộ nhớ mỗi khi bạn sử dụng; chỉ khi ai đó có thể đã sửa đổi nó từ tên của nó .

Do đó (&thing.a)[1]có thể được giả định bởi trình biên dịch để không tham chiếu đến thing.b. Nó có thể sử dụng thực tế này để sắp xếp lại các lần đọc và ghi vàothing.b , làm mất hiệu lực những gì bạn muốn nó làm mà không làm mất hiệu lực những gì bạn thực sự yêu cầu nó làm.

Một ví dụ cổ điển về điều này là loại bỏ const.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

ở đây bạn thường nhận được một trình biên dịch nói 7 rồi 2! = 7, và sau đó là hai con trỏ giống nhau; mặc dù thực tế ptrđang chỉ vào x. Trình biên dịch thực tế xlà một giá trị không đổi để không bận tâm đọc nó khi bạn yêu cầu giá trị củax .

Nhưng khi bạn lấy địa chỉ của x, bạn buộc nó phải tồn tại. Sau đó, bạn loại bỏ const và sửa đổi nó. Vì vậy, vị trí thực tế trong bộ nhớ nơi xđã được sửa đổi, trình biên dịch có thể không thực sự đọc nó khi đọc x!

Trình biên dịch có thể đủ thông minh để tìm ra cách thậm chí tránh theo dõi ptrđể đọc *ptr, nhưng thường thì không. Hãy sử dụng và sử dụng ptr = ptr+argc-1, nếu không bạn sẽ nhầm lẫn nếu trình tối ưu hóa ngày càng thông minh hơn bạn.

Bạn có thể cung cấp một tùy chỉnh operator[]để có được mặt hàng phù hợp.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

có cả hai đều hữu ích.


"thực tế là nó không phải là thành viên của một mảng có nghĩa là trình biên dịch có thể giả định rằng không có quyền truy cập mảng dựa trên [] nào có thể sửa đổi nó." - không đúng, ví dụ: (&thing.a)[0]có thể sửa đổi nó
MM

Tôi không thấy ví dụ const có liên quan gì đến câu hỏi. Điều đó không thành công chỉ vì có một quy tắc cụ thể mà một đối tượng const có thể không được sửa đổi, không phải bất kỳ lý do nào khác.
MM

1
@MM, nó không phải là một ví dụ về lập chỉ mục vào một cấu trúc, nhưng nó là một minh họa rất tốt về cách sử dụng hành vi không xác định để tham chiếu một thứ gì đó theo vị trí rõ ràng của nó trong bộ nhớ, có thể dẫn đến kết quả khác với mong đợi, bởi vì trình biên dịch có thể làm điều gì đó khác với UB hơn bạn muốn.
tự đại diện

@MM Rất tiếc, không có quyền truy cập mảng nào ngoài một truy cập tầm thường thông qua một con trỏ đến chính đối tượng. Và điều thứ hai chỉ là một ví dụ về tác dụng phụ dễ thấy của hành vi không xác định; trình biên dịch tối ưu hóa các lần đọc xvì nó biết bạn không thể thay đổi nó theo cách đã xác định. Tối ưu hóa tương tự có thể xảy ra khi bạn thay đổi bthông qua (&blah.a)[1]nếu trình biên dịch có thể chứng minh rằng không có quyền truy cập xác định bnào có thể thay đổi nó; một thay đổi như vậy có thể xảy ra do những thay đổi có vẻ vô hại trong trình biên dịch, mã xung quanh hoặc bất cứ điều gì. Vì vậy, ngay cả việc kiểm tra xem nó có hoạt động hay không vẫn chưa đủ.
Yakk - Adam Nevraumont

6

Đây là một cách để sử dụng một lớp proxy để truy cập các phần tử trong một mảng thành viên theo tên. Nó rất C ++ và không có lợi ích gì so với các hàm truy cập ref-return, ngoại trừ ưu tiên cú pháp. Điều này làm quá tải ->toán tử để truy cập các phần tử với tư cách là thành viên, vì vậy để có thể chấp nhận được, người ta cần vừa không thích cú pháp của accessors ( d.a() = 5;), vừa chấp nhận việc sử dụng-> với một đối tượng không phải là con trỏ. Tôi hy vọng điều này cũng có thể khiến người đọc không quen thuộc với mã này nhầm lẫn, vì vậy đây có thể là một mẹo nhỏ hơn là một thứ bạn muốn đưa vào sản xuất.

Cấu Datatrúc trong mã này cũng bao gồm các quá tải cho toán tử chỉ số con, để truy cập các phần tử được lập chỉ mục bên trong arthành viên mảng của nó , cũng như beginvà các endhàm, để lặp lại. Ngoài ra, tất cả những thứ này đều bị quá tải với các phiên bản không phải const và const, mà tôi cảm thấy cần phải được đưa vào để hoàn thiện.

Khi Data's ->được sử dụng để truy cập một phần tử theo tên (như thế này my_data->b = 5;:), một Proxyđối tượng sẽ được trả về. Sau đó, bởi vì giá trị này Proxykhông phải là một con trỏ, ->toán tử của chính nó được gọi là chuỗi tự động, trả về một con trỏ cho chính nó. Bằng cách này, Proxyđối tượng được khởi tạo và vẫn hợp lệ trong quá trình đánh giá biểu thức ban đầu.

Xây dựng của một Proxyđối tượng populates 3 thành viên tham chiếu của nó a, bctheo một con trỏ thông qua trong các nhà xây dựng, được giả định là điểm đến một bộ đệm chứa giá trị ít nhất 3 có loại được cho là các mẫu tham số T. Vì vậy, thay vì sử dụng các tham chiếu được đặt tên là thành viên của Datalớp, điều này sẽ tiết kiệm bộ nhớ bằng cách điền các tham chiếu tại điểm truy cập (nhưng thật không may, sử dụng ->chứ không phải. toán tử).

Để kiểm tra xem trình tối ưu hóa của trình biên dịch loại bỏ tất cả các hướng dẫn được giới thiệu bằng cách sử dụng tốt như thế nào Proxy, đoạn mã dưới đây bao gồm 2 phiên bản của main(). Các #if 1phiên bản sử dụng ->[]khai thác, và #if 0Thực hiện phiên bản tập tương đương với các thủ tục, nhưng chỉ bằng cách trực tiếp truy cậpData::ar .

Các Nci()chức năng tạo ra các giá trị số nguyên thời gian chạy cho khởi tạo các phần tử mảng, mà ngăn cản tôi ưu hoa từ chỉ cắm giá trị không đổi trực tiếp vào mỗistd::cout << cuộc gọi.

Đối với gcc 6.2, sử dụng -O3, cả hai phiên bản main()tạo ra cùng một hội đồng (chuyển đổi giữa #if 1#if 0trước phiên bản đầu tiên main()để so sánh): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

Xấu. Được ủng hộ chủ yếu vì bạn đã chứng minh rằng điều này tối ưu hóa. BTW, bạn có thể làm điều đó dễ dàng hơn nhiều bằng cách viết một hàm rất đơn giản, không phải toàn bộ main()với các hàm thời gian! ví dụ int getb(Data *d) { return (*d)->b; }biên dịch thành just mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Vâng, Data &dsẽ làm cho cú pháp dễ dàng hơn, nhưng tôi đã sử dụng một con trỏ thay vì ref để làm nổi bật sự kì quái của quá tải ->theo cách này.)
Peter Cordes

Dù sao, điều này là mát mẻ. Những ý tưởng khác như int tmp[] = { a, b, c}; return tmp[idx];không tối ưu hóa đi, vì vậy nó là gọn gàng để làm điều này.
Peter Cordes

Một lý do nữa mà tôi bỏ lỡ operator.trong C ++ 17.
Jens

2

Nếu việc đọc các giá trị là đủ và hiệu quả không phải là mối quan tâm hoặc nếu bạn tin tưởng trình biên dịch của mình sẽ tối ưu hóa mọi thứ tốt hoặc nếu cấu trúc chỉ là 3 byte, bạn có thể thực hiện điều này một cách an toàn:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Đối với phiên bản chỉ dành cho C ++, bạn có thể muốn sử dụng static_assertđể xác minh rằng struct datacó bố cục tiêu chuẩn và có thể ném ngoại lệ trên chỉ mục không hợp lệ thay thế.


1

Đó là bất hợp pháp, nhưng có một cách giải quyết:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Bây giờ bạn có thể lập chỉ mục v:


6
Nhiều dự án c ++ nghĩ rằng việc giảm dự báo ở khắp nơi là tốt. Chúng ta vẫn không nên rao giảng những thực hành xấu.
Người kể chuyện - Unslander Monica

2
Liên minh giải quyết vấn đề bí danh nghiêm ngặt bằng cả hai ngôn ngữ. Nhưng gõ punning through union chỉ tốt trong C, không tốt trong C ++.
Lundin

1
Tuy nhiên, tôi sẽ không ngạc nhiên nếu điều này hoạt động trên 100% tất cả các trình biên dịch c ++. không bao giờ.
Sven Nilsson

1
Bạn có thể thử nó trong gcc khi bật cài đặt trình tối ưu hóa tích cực nhất.
Lundin

1
@Lundin: punning type union là hợp pháp trong GNU C ++, như một phần mở rộng của ISO C ++. Nó dường như không được nêu rõ ràng trong sách hướng dẫn , nhưng tôi khá chắc chắn về điều này. Tuy nhiên, câu trả lời này cần giải thích đâu là hợp lệ và đâu là không hợp lệ.
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.