Tại sao điều này được tuyên bố là trình biên dịch con trỏ cảnh báo cụ thể kiểu trình biên dịch cụ thể?


38

Tôi đã đọc nhiều bài viết khác nhau về Stack Overflow RE: lỗi con trỏ bị trừng phạt loại. Tôi hiểu rằng lỗi về cơ bản là cảnh báo trình biên dịch về sự nguy hiểm của việc truy cập một đối tượng thông qua một con trỏ thuộc loại khác (mặc dù có một ngoại lệ được tạo ra char*), đây là một cảnh báo dễ hiểu và hợp lý.

Câu hỏi của tôi là cụ thể cho mã dưới đây: tại sao việc chuyển địa chỉ của một con trỏ thành void**đủ điều kiện cho cảnh báo này (được quảng cáo thành lỗi thông qua -Werror)?

Hơn nữa, mã này được biên dịch cho nhiều kiến ​​trúc đích, chỉ một trong số đó tạo ra cảnh báo / lỗi - điều này có thể ngụ ý rằng nó là một sự thiếu sót cụ thể của phiên bản trình biên dịch?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Nếu sự hiểu biết của tôi, được nêu ở trên, là chính xác, a void**, vẫn chỉ là một con trỏ, đây sẽ là đúc an toàn.

Có một cách giải quyết không sử dụng các giá trị sẽ làm dịu cảnh báo / lỗi cụ thể của trình biên dịch này không? Tức là tôi hiểu điều đó và tại sao điều này sẽ giải quyết vấn đề, nhưng tôi muốn tránh cách tiếp cận này vì tôi muốn tận dụng lợi thế của freeFunc() NULL trong một dự định ngoài luồng:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Trình biên dịch vấn đề (một trong một):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Trình biên dịch không phàn nàn (một trong nhiều):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Cập nhật: Tôi đã phát hiện thêm cảnh báo dường như được tạo cụ thể khi được biên dịch bằng -O2(chỉ với "trình biên dịch sự cố" đã lưu ý)



1
"a void**, vẫn chỉ là một con trỏ, đây nên là đúc an toàn." Ái chà có skippy! Âm thanh như bạn có một số giả định cơ bản đang diễn ra. Cố gắng suy nghĩ ít hơn về byte và đòn bẩy và nhiều hơn về mặt trừu tượng, bởi vì đó là những gì bạn thực sự đang lập trình với
Các cuộc đua Lightness trong Orbit

7
Tiếp theo, trình biên dịch bạn đang sử dụng là 1517 tuổi! Tôi sẽ không dựa vào một trong hai.
Tavian Barnes

4
@TavianBarnes Ngoài ra, nếu bạn phải dựa vào GCC 3 vì bất kỳ lý do gì, tốt nhất là sử dụng phiên bản kết thúc cuối đời, đó là 3.4.6, tôi nghĩ vậy. Tại sao không tận dụng tất cả các bản sửa lỗi có sẵn cho loạt đó trước khi nó được đặt để nghỉ ngơi.
Kaz

Loại tiêu chuẩn mã hóa C ++ nào quy định tất cả các không gian đó?
Peter Mortensen

Câu trả lời:


33

Một giá trị của kiểu void**là một con trỏ tới một đối tượng của kiểu void*. Một đối tượng của loại Foo*không phải là một đối tượng của loại void*.

Có một chuyển đổi ngầm giữa các giá trị của loại Foo*void*. Chuyển đổi này có thể thay đổi đại diện của giá trị. Tương tự, bạn có thể viết int n = 3; double x = n;và điều này có hành vi được xác định rõ là cài đặt xgiá trị 3.0, nhưng double *p = (double*)&n;có hành vi không xác định (và trong thực tế sẽ không được đặt pthành một con trỏ của con trỏ thành hình 3.0chữ nhật trên bất kỳ kiến ​​trúc chung nào).

Các kiến ​​trúc nơi các loại con trỏ khác nhau đến các đối tượng có các biểu diễn khác nhau ngày nay rất hiếm, nhưng chúng được cho phép theo tiêu chuẩn C. Có (hiếm) máy cũ có con trỏ từ là địa chỉ của một từ trong bộ nhớ và con trỏ byte là địa chỉ của một từ cùng với một byte bù trong từ này; Foo*sẽ là một con trỏ từ và void*sẽ là một con trỏ byte trên các kiến ​​trúc như vậy. Có những máy (hiếm) có con trỏ béo chứa thông tin không chỉ về địa chỉ của đối tượng, mà còn về loại, kích thước và danh sách kiểm soát truy cập của nó; một con trỏ tới một kiểu xác định có thể có một biểu diễn khác với một void*loại cần thêm thông tin loại trong thời gian chạy.

Những máy như vậy rất hiếm, nhưng được phép theo tiêu chuẩn C. Và một số trình biên dịch C tận dụng sự cho phép để coi các con trỏ bị loại là khác biệt để tối ưu hóa mã. Nguy cơ của răng cưa là một hạn chế lớn đối với khả năng tối ưu hóa mã của trình biên dịch, vì vậy trình biên dịch có xu hướng tận dụng các quyền đó.

Trình biên dịch có thể tự do nói với bạn rằng bạn đang làm gì đó sai hoặc lặng lẽ làm những gì bạn không muốn hoặc lặng lẽ làm những gì bạn muốn. Hành vi không xác định cho phép bất kỳ trong số này.

Bạn có thể tạo freefuncmột macro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Điều này đi kèm với các hạn chế thông thường của macro: thiếu an toàn loại, pđược đánh giá hai lần. Lưu ý rằng điều này chỉ mang lại cho bạn sự an toàn khi không để lại các con trỏ lơ lửng xung quanh nếu plà con trỏ đơn tới đối tượng được giải phóng.


1
Và thật tốt khi biết rằng ngay cả khi Foo*void*có cùng một đại diện trên kiến ​​trúc của bạn, nó vẫn không được xác định để loại bỏ chúng.
Tavian Barnes

12

A void *được xử lý đặc biệt theo tiêu chuẩn C một phần vì nó tham chiếu một loại không hoàn chỉnh. Phương pháp này không không mở rộng đến void **vì nó làm điểm đến một loại hoàn chỉnh, cụ thể void *.

Các quy tắc bí danh nghiêm ngặt nói rằng bạn không thể chuyển đổi một con trỏ của một loại thành một con trỏ của một loại khác và sau đó đã loại bỏ con trỏ đó bởi vì làm như vậy có nghĩa là diễn giải lại các byte của một loại như một loại khác. Ngoại lệ duy nhất là khi chuyển đổi sang loại ký tự cho phép bạn đọc biểu diễn của một đối tượng.

Bạn có thể khắc phục giới hạn này bằng cách sử dụng macro giống như hàm thay vì hàm:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Mà bạn có thể gọi như thế này:

freeFunc(f);

Tuy nhiên, điều này có một hạn chế, vì macro trên sẽ đánh giá objhai lần. Nếu bạn đang sử dụng GCC, có thể tránh điều này với một số tiện ích mở rộng, cụ thể là typeoftừ khóa và biểu thức câu lệnh:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
+1 để cung cấp thực hiện tốt hơn các hành vi dự định. Vấn đề duy nhất tôi thấy với #define, đó là nó sẽ đánh giá objhai lần. Tôi không biết một cách tốt để tránh đánh giá thứ hai, mặc dù. Ngay cả một biểu thức câu lệnh (phần mở rộng GNU) sẽ không thực hiện thủ thuật như bạn cần gán objsau khi bạn đã sử dụng giá trị của nó.
cmaster - phục hồi monica

2
@cmaster: Nếu bạn sẵn sàng sử dụng các phần mở rộng GNU như biểu thức câu lệnh, thì bạn có thể sử dụng typeofđể tránh đánh giá objhai lần : #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh

@ruakh Rất tuyệt :-) Sẽ rất tuyệt nếu dbush sẽ chỉnh sửa câu trả lời đó, vì vậy nó sẽ không bị xóa hàng loạt với các bình luận.
cmaster - phục hồi monica

9

Hủy bỏ hội nghị một loại con trỏ bị trừng phạt là UB và bạn không thể tin vào những gì sẽ xảy ra.

Các trình biên dịch khác nhau tạo ra các cảnh báo khác nhau và vì mục đích này, các phiên bản khác nhau của cùng một trình biên dịch có thể được coi là các trình biên dịch khác nhau. Đây có vẻ là một lời giải thích tốt hơn cho phương sai mà bạn nhìn thấy hơn là sự phụ thuộc vào kiến ​​trúc.

Một trường hợp có thể giúp bạn hiểu tại sao loại picky trong trường hợp này có thể xấu là chức năng của bạn sẽ không hoạt động trên một kiến ​​trúc sizeof(Foo*) != sizeof(void*). Điều đó được cho phép theo tiêu chuẩn mặc dù tôi không biết bất kỳ điều gì hiện tại mà điều này là đúng.

Một cách giải quyết khác là sử dụng macro thay vì hàm.

Lưu ý rằng freechấp nhận con trỏ null.


2
Hấp dẫn rằng nó có thể đó sizeof Foo* != sizeof void*. Tôi chưa bao giờ gặp phải kích thước con trỏ "trong tự nhiên" phụ thuộc vào loại, vì vậy trong nhiều năm qua, tôi đã xem xét điều đó là tiên đề rằng kích thước con trỏ hoàn toàn giống nhau trên một kiến ​​trúc nhất định.
StoneThrow

1
@Stonethrow ví dụ tiêu chuẩn là con trỏ chất béo được sử dụng để giải quyết các byte trong kiến ​​trúc địa chỉ từ. Nhưng tôi nghĩ rằng các máy có thể đánh địa chỉ từ hiện tại sử dụng từ sizeof char == sizeof từ .
AProgrammer

2
Lưu ý rằng loại phải được ngoặc đơn cho sizeof ...
Antti Haapala

@StoneThrow: Bất kể kích thước con trỏ, phân tích bí danh dựa trên loại làm cho nó không an toàn; điều này giúp trình biên dịch tối ưu hóa bằng cách giả sử rằng một cửa hàng thông qua việc float*không sửa đổi một int32_tđối tượng, do đó, int32_t*không cần int32_t *restrict ptrtrình biên dịch giả định rằng nó không trỏ đến cùng một bộ nhớ. Tương tự cho các cửa hàng thông qua void**việc được giả định không sửa đổi một Foo*đối tượng.
Peter Cordes

4

Mã này không hợp lệ theo Tiêu chuẩn C, do đó, nó có thể hoạt động trong một số trường hợp, nhưng không nhất thiết phải là di động.

"Quy tắc bí danh nghiêm ngặt" để truy cập một giá trị thông qua một con trỏ đã được chuyển sang một loại con trỏ khác được tìm thấy trong 6.5 đoạn 7:

Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bằng biểu thức lvalue có một trong các loại sau:

  • một loại tương thích với loại hiệu quả của đối tượng,

  • một phiên bản đủ điều kiện của một loại tương thích với loại hiệu quả của đối tượng,

  • một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,

  • một loại là loại đã ký hoặc không dấu tương ứng với một phiên bản đủ điều kiện của loại đối tượng hiệu quả,

  • một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên giữa các thành viên của nó (bao gồm, đệ quy, một thành viên của liên hiệp hoặc phân nhóm), hoặc

  • một kiểu nhân vật.

Trong *obj = NULL;câu lệnh của bạn , đối tượng có kiểu hiệu quả Foo*nhưng được truy cập bằng biểu thức lvalue *objvới kiểu void*.

Trong 6.7.5.1 đoạn 2, chúng ta có

Để hai loại con trỏ tương thích, cả hai sẽ có đủ điều kiện nhận dạng và cả hai sẽ là con trỏ cho các loại tương thích.

Vì vậy, void*Foo*không phải là loại tương thích hoặc loại tương thích với vòng loại được thêm vào, và chắc chắn không phù hợp với bất kỳ tùy chọn nào khác của quy tắc bí danh nghiêm ngặt.

Mặc dù không phải là lý do kỹ thuật mà mã không hợp lệ, nhưng cũng có liên quan để lưu ý phần 6.2.5 đoạn 26:

Một con trỏ voidsẽ có cùng các yêu cầu biểu diễn và căn chỉnh giống như một con trỏ tới một kiểu ký tự. Tương tự, các con trỏ tới các phiên bản đủ điều kiện hoặc không đủ tiêu chuẩn của các loại tương thích sẽ có cùng các yêu cầu về đại diện và căn chỉnh. Tất cả các con trỏ đến các loại cấu trúc sẽ có cùng các yêu cầu đại diện và căn chỉnh như nhau. Tất cả các con trỏ tới các loại kết hợp sẽ có cùng các yêu cầu đại diện và căn chỉnh như nhau. Con trỏ đến các loại khác không cần phải có cùng yêu cầu đại diện hoặc căn chỉnh.

Đối với sự khác biệt trong các cảnh báo, đây không phải là trường hợp Tiêu chuẩn yêu cầu thông báo chẩn đoán, do đó, vấn đề là trình biên dịch hoặc phiên bản của nó tốt đến mức nào để nhận thấy các vấn đề tiềm ẩn và chỉ ra chúng theo cách hữu ích. Bạn nhận thấy cài đặt tối ưu hóa có thể tạo sự khác biệt. Điều này thường là do nhiều thông tin được tạo ra trong nội bộ về cách các phần khác nhau của chương trình thực sự khớp với nhau trong thực tế và do đó thông tin bổ sung cũng có sẵn để kiểm tra cảnh báo.


2

Trên hết những gì các câu trả lời khác đã nói, đây là một kiểu chống cổ điển trong C, và một thứ nên được đốt bằng lửa. Nó xuất hiện trong:

  1. Các chức năng miễn phí và không có chức năng như chức năng bạn đã tìm thấy cảnh báo.
  2. Các hàm phân bổ tránh xa thành ngữ C tiêu chuẩn của việc trả về void *(không gặp phải vấn đề này vì nó liên quan đến chuyển đổi giá trị thay vì loại picky ), thay vào đó trả về một cờ lỗi và lưu trữ kết quả thông qua con trỏ tới con trỏ.

Đối với một ví dụ khác về (1), đã có một trường hợp khét tiếng lâu đời trong av_freechức năng của ffmpeg / libavcodec . Tôi tin rằng cuối cùng nó đã được sửa với một macro hoặc một số thủ thuật khác, nhưng tôi không chắc chắn.

Đối với (2), cả hai cudaMallocposix_memalignlà ví dụ.

Trong cả hai trường hợp, giao diện vốn không yêu cầu sử dụng không hợp lệ, nhưng nó khuyến khích mạnh mẽ và chỉ thừa nhận sử dụng đúng với một đối tượng tạm thời loại void *đánh bại mục đích của chức năng miễn phí và làm cho việc phân bổ trở nên khó xử.


Bạn có một liên kết giải thích thêm về lý do tại sao (1) là một mô hình chống? Tôi không nghĩ rằng tôi quen thuộc với tình huống / tranh luận này và muốn tìm hiểu thêm.
StoneThrow

1
@StoneThrow: Nó thực sự đơn giản - mục đích là để ngăn chặn việc sử dụng sai bằng cách vô hiệu hóa đối tượng lưu trữ con trỏ vào bộ nhớ đang được giải phóng, nhưng cách duy nhất nó thực sự có thể làm là nếu người gọi thực sự lưu trữ con trỏ trong một đối tượng gõ void *và chuyển đổi / chuyển đổi nó mỗi khi nó muốn hủy đăng ký nó. Điều này rất khó xảy ra. Nếu người gọi đang lưu trữ một số loại con trỏ khác, cách duy nhất để gọi hàm mà không gọi UB là sao chép con trỏ vào một đối tượng tạm thời của kiểu void *và chuyển địa chỉ của hàm đó sang hàm giải phóng, và sau đó nó chỉ ...
R .. GitHub DỪNG GIÚP ICE

1
... null một đối tượng tạm thời thay vì lưu trữ thực tế nơi người gọi có con trỏ. Tất nhiên những gì thực sự xảy ra là người dùng của chức năng cuối cùng thực hiện một (void **)vai, tạo ra hành vi không xác định.
R .. GitHub DỪNG GIÚP ICE

2

Mặc dù C được thiết kế cho các máy sử dụng cùng một đại diện cho tất cả các con trỏ, các tác giả của Tiêu chuẩn muốn làm cho ngôn ngữ có thể sử dụng được trên các máy sử dụng các cách biểu diễn khác nhau cho con trỏ tới các loại đối tượng khác nhau. Do đó, họ không yêu cầu các máy sử dụng các biểu diễn con trỏ khác nhau cho các loại con trỏ khác nhau hỗ trợ loại "con trỏ tới bất kỳ loại con trỏ nào", mặc dù nhiều máy có thể làm như vậy với chi phí bằng không.

Trước khi Tiêu chuẩn được viết, việc triển khai cho các nền tảng sử dụng cùng một đại diện cho tất cả các loại con trỏ sẽ nhất trí cho phép void**sử dụng, ít nhất là với việc truyền phù hợp, như một "con trỏ tới bất kỳ con trỏ nào". Các tác giả của Tiêu chuẩn gần như chắc chắn nhận ra rằng điều này sẽ hữu ích trên các nền tảng hỗ trợ nó, nhưng vì nó không thể được hỗ trợ toàn cầu nên họ đã từ chối ủy quyền. Thay vào đó, họ hy vọng rằng việc triển khai chất lượng sẽ xử lý các cấu trúc như những gì Cơ sở lý luận sẽ mô tả như là một "phần mở rộng phổ biến", trong trường hợp làm như vậy sẽ có ý nghĩa.

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.