Triết lý đằng sau hành vi không xác định


59

Thông số kỹ thuật của C \ C ++ bỏ qua một số lượng lớn các hành vi mở cho trình biên dịch thực hiện theo cách riêng của chúng. Có một số câu hỏi luôn luôn được hỏi ở đây về cùng và chúng tôi có một số bài viết tuyệt vời về nó:

Câu hỏi của tôi không phải là về hành vi không xác định là gì, hoặc nó thực sự xấu. Tôi biết những nguy hiểm và hầu hết các trích dẫn hành vi không xác định có liên quan từ tiêu chuẩn, vì vậy xin vui lòng không đăng câu trả lời về mức độ xấu của nó. Câu hỏi này là về triết lý đằng sau để lại rất nhiều hành vi mở cho việc thực hiện trình biên dịch.

Tôi đọc một bài đăng blog tuyệt vời nói rằng hiệu suất là lý do chính. Tôi đã tự hỏi nếu hiệu suất là tiêu chí duy nhất để cho phép nó, hoặc có bất kỳ yếu tố nào khác ảnh hưởng đến quyết định để mọi thứ mở cho việc thực hiện trình biên dịch?

Nếu bạn có bất kỳ ví dụ nào để trích dẫn về cách một hành vi không xác định cụ thể cung cấp đủ chỗ cho trình biên dịch để tối ưu hóa, vui lòng liệt kê chúng. Nếu bạn biết bất kỳ yếu tố nào khác ngoài hiệu suất, vui lòng quay lại câu trả lời của bạn với đủ chi tiết.

Nếu bạn không hiểu câu hỏi hoặc không có đủ bằng chứng / nguồn để trả lời câu trả lời của bạn, xin vui lòng không đăng rộng rãi câu trả lời suy đoán.


7
Ai đã từng nghe nói về một máy tính xác định nào?
sova

1
như các lập trình viên câu trả lời xuất sắc của litb.stackexchange.com/a/99741/192238 chỉ ra, tiêu đề và nội dung của câu hỏi này có vẻ hơi không khớp: "các hành vi mở cho trình biên dịch thực hiện theo cách riêng của chúng" thường được gọi là định nghĩa thực hiện . chắc chắn, UB thực tế được phép được xác định bởi tác giả triển khai, nhưng thường xuyên hơn không, họ không bận tâm (và tối ưu hóa tất cả, v.v.)
underscore_d

Câu trả lời:


49

Đầu tiên, tôi sẽ lưu ý rằng mặc dù tôi chỉ đề cập đến "C" ở đây, điều tương tự cũng thực sự áp dụng tương tự với C ++.

Nhận xét đề cập đến Godel là một phần (nhưng chỉ một phần) về điểm.

Khi bạn đi xuống, hành vi không xác định trong các tiêu chuẩn C phần lớn chỉ là chỉ ra ranh giới giữa những gì các tiêu chuẩn cố gắng xác định và những gì nó không.

Các định lý của Godel (có hai) về cơ bản nói rằng không thể định nghĩa một hệ thống toán học có thể được chứng minh (bằng các quy tắc riêng của nó) để hoàn chỉnh và nhất quán. Bạn có thể đưa ra các quy tắc của mình để có thể hoàn thành (trường hợp anh ấy xử lý là quy tắc "bình thường" cho các số tự nhiên) hoặc nếu không bạn có thể chứng minh tính nhất quán của nó, nhưng bạn không thể có cả hai.

Trong trường hợp của một cái gì đó như C, điều đó không áp dụng trực tiếp - đối với hầu hết các phần, "khả năng chứng minh" về tính hoàn chỉnh hoặc tính nhất quán của hệ thống không phải là ưu tiên cao đối với hầu hết các nhà thiết kế ngôn ngữ. Đồng thời, vâng, có lẽ họ đã bị ảnh hưởng (ít nhất là ở một mức độ nào đó) khi biết rằng không thể xác định một hệ thống "hoàn hảo" - một hệ thống hoàn toàn có thể chứng minh được. Biết rằng một điều như vậy là không thể có thể làm cho việc lùi lại, thở một chút và quyết định giới hạn của những gì họ sẽ cố gắng xác định sẽ dễ dàng hơn một chút.

Có nguy cơ (một lần nữa) bị buộc tội kiêu ngạo, tôi mô tả tiêu chuẩn C là bị chi phối (một phần) bởi hai ý tưởng cơ bản:

  1. Ngôn ngữ nên hỗ trợ càng nhiều phần cứng càng tốt (lý tưởng nhất là tất cả phần cứng "lành mạnh" xuống giới hạn thấp hơn hợp lý).
  2. Ngôn ngữ nên hỗ trợ viết càng nhiều phần mềm càng tốt cho môi trường nhất định.

Điều đầu tiên có nghĩa là nếu ai đó định nghĩa một CPU mới, thì có thể cung cấp một triển khai C tốt, chắc chắn, có thể sử dụng được cho điều đó, miễn là thiết kế rơi ít nhất là gần với một vài hướng dẫn đơn giản - về cơ bản, nếu nó tuân theo thứ gì đó theo thứ tự chung của mô hình Von Neumann và cung cấp ít nhất một lượng bộ nhớ tối thiểu hợp lý, đủ để cho phép thực hiện C. Để triển khai "được lưu trữ" (một chương trình chạy trên HĐH), bạn cần hỗ trợ một số khái niệm tương ứng chặt chẽ với các tệp và có một bộ ký tự với một bộ ký tự tối thiểu nhất định (yêu cầu 91).

Thứ hai có nghĩa là có thể viết mã thao tác trực tiếp với phần cứng, do đó bạn có thể viết những thứ như bộ tải khởi động, hệ điều hành, phần mềm nhúng chạy mà không cần bất kỳ HĐH nào, v.v ... Cuối cùng cũng có một số giới hạn hệ thống thực tiễn điều hành, bộ nạp khởi động, vv, là khả năng chứa ít nhất một chút chút mã viết bằng ngôn ngữ lắp ráp. Tương tự như vậy, ngay cả một hệ thống nhúng nhỏ cũng có khả năng bao gồm ít nhất một số loại thói quen thư viện được viết sẵn để cấp quyền truy cập vào các thiết bị trên hệ thống máy chủ. Mặc dù một ranh giới chính xác rất khó xác định, mục đích là sự phụ thuộc vào mã đó phải được giữ ở mức tối thiểu.

Hành vi không xác định trong ngôn ngữ chủ yếu được thúc đẩy bởi ý định cho ngôn ngữ hỗ trợ các khả năng này. Ví dụ: ngôn ngữ cho phép bạn chuyển đổi một số nguyên tùy ý thành một con trỏ và truy cập bất cứ điều gì xảy ra tại địa chỉ đó. Tiêu chuẩn không cố gắng nói điều gì sẽ xảy ra khi bạn làm (ví dụ, thậm chí đọc từ một số địa chỉ có thể có ảnh hưởng bên ngoài). Đồng thời, nó không cố gắng ngăn bạn làm những việc như vậy, bởi vì bạn cần phải có một số loại phần mềm mà bạn có thể viết bằng C.

Có một số hành vi không xác định được thúc đẩy bởi các yếu tố thiết kế khác là tốt. Ví dụ, một mục đích khác của C là hỗ trợ biên dịch riêng. Điều này có nghĩa (ví dụ) rằng nó có ý định rằng bạn có thể "liên kết" các mảnh lại với nhau bằng cách sử dụng một trình liên kết theo hầu hết những gì chúng ta xem là mô hình thông thường của trình liên kết. Cụ thể, có thể kết hợp các mô-đun được biên dịch riêng biệt thành một chương trình hoàn chỉnh mà không cần biết về ngữ nghĩa của ngôn ngữ.

Có một loại hành vi không xác định khác (phổ biến hơn nhiều trong C ++ so với C), hiện diện đơn giản là do các giới hạn về công nghệ trình biên dịch - những điều mà về cơ bản chúng ta biết là lỗi và có thể muốn trình biên dịch chẩn đoán là lỗi, nhưng với các giới hạn hiện tại về công nghệ trình biên dịch, điều đáng nghi ngờ là chúng có thể được chẩn đoán trong mọi trường hợp. Nhiều trong số này được điều khiển bởi các yêu cầu khác, chẳng hạn như để biên dịch riêng biệt, do đó, phần lớn là vấn đề cân bằng các yêu cầu xung đột, trong trường hợp ủy ban thường chọn hỗ trợ các khả năng lớn hơn, ngay cả khi điều đó có nghĩa là thiếu chẩn đoán một số vấn đề có thể xảy ra, thay vì giới hạn các khả năng để đảm bảo rằng tất cả các vấn đề có thể được chẩn đoán.

Những khác biệt về ý định này thúc đẩy hầu hết sự khác biệt giữa C và một cái gì đó như Java hoặc các hệ thống dựa trên CLI của Microsoft. Cái sau khá hạn chế để làm việc với một bộ phần cứng hạn chế hơn nhiều, hoặc yêu cầu phần mềm phải mô phỏng phần cứng cụ thể hơn mà chúng nhắm tới. Họ cũng đặc biệt có ý định ngăn chặn mọi thao tác trực tiếp đối với phần cứng, thay vào đó yêu cầu bạn sử dụng một cái gì đó như JNI hoặc P / Gọi (và mã được viết bằng thứ gì đó như C) để thực hiện một nỗ lực như vậy.

Quay trở lại các định lý của Godel một lúc, chúng ta có thể rút ra một điều song song: Java và CLI đã chọn phương án "nhất quán nội bộ", trong khi C đã chọn phương án "hoàn chỉnh". Tất nhiên, đây là một loại suy rất thô - Tôi nghi ngờ ai ấy cố gắng một bằng chứng chính thức hoặc là nhất quán nội bộ hoặc tính đầy đủ trong cả hai trường hợp. Tuy nhiên, khái niệm chung không phù hợp khá chặt chẽ với những lựa chọn mà họ đã thực hiện.


25
Tôi nghĩ rằng Định lý của Godel là một cá trích đỏ. Họ đối phó với việc chứng minh một hệ thống từ các tiên đề của chính nó, không phải là trường hợp ở đây: C không cần phải được chỉ định trong C. Hoàn toàn có thể có một ngôn ngữ được chỉ định hoàn toàn (xem xét một máy Turing).
poolie

9
Xin lỗi, nhưng tôi sợ bạn đã hoàn toàn hiểu sai Định lý của Godel. Họ đối phó với sự bất khả thi trong việc chứng minh tất cả các tuyên bố đúng trong một hệ thống logic nhất quán; về mặt điện toán, định lý không hoàn chỉnh tương tự như nói rằng có những vấn đề không thể giải quyết bằng bất kỳ chương trình nào - các vấn đề tương tự như các phát biểu đúng, các chương trình để chứng minh và mô hình tính toán cho hệ thống logic. Nó không có kết nối nào với hành vi không xác định. Xem để được giải thích về sự tương tự ở đây: scottaaronson.com/blog/?p=710 .
Alex ten Brink

5
Tôi nên lưu ý rằng không cần có máy Von Neumann để thực hiện C. Hoàn toàn có thể (và thậm chí không khó lắm) để phát triển triển khai C cho kiến ​​trúc Harvard (và tôi sẽ không ngạc nhiên khi thấy rất nhiều triển khai như vậy trên các hệ thống nhúng)
bdonlan

1
Thật không may, triết lý trình biên dịch C hiện đại đưa UB lên một cấp độ hoàn toàn mới. Ngay cả trong trường hợp một chương trình đã được chuẩn bị để đối phó với hầu hết các hậu quả "tự nhiên" hợp lý từ một dạng Hành vi không xác định cụ thể và những chương trình mà nó không thể giải quyết ít nhất sẽ có thể nhận ra (ví dụ như tràn số nguyên bị mắc kẹt), triết lý mới ủng hộ bỏ qua bất kỳ mã nào không thể thực thi trừ khi UB sẽ xảy ra, biến mã có thể hoạt động chính xác trên bất kỳ triển khai nào thành mã "hiệu quả hơn" nhưng hoàn toàn sai.
supercat

20

Các C lý do giải thích

Các thuật ngữ hành vi không xác định, hành vi không xác định và hành vi được xác định thực hiện được sử dụng để phân loại kết quả của việc viết chương trình có các thuộc tính mà Tiêu chuẩn không hoặc không thể mô tả hoàn toàn. Mục tiêu của việc áp dụng phân loại này là cho phép một số loại nhất định trong số các triển khai cho phép chất lượng thực hiện trở thành một lực lượng tích cực trên thị trường cũng như cho phép các tiện ích mở rộng phổ biến nhất định mà không cần loại bỏ bộ đệm tuân thủ Tiêu chuẩn. Phụ lục F của Danh mục tiêu chuẩn những hành vi thuộc một trong ba loại này.

Hành vi không xác định cung cấp cho người thực hiện một số vĩ độ trong việc dịch các chương trình. Vĩ độ này không kéo dài đến mức không dịch được chương trình.

Hành vi không xác định cung cấp cho giấy phép người thực hiện không bắt lỗi nhất định của chương trình khó chẩn đoán. Nó cũng xác định các khu vực có thể mở rộng ngôn ngữ phù hợp: người triển khai có thể tăng ngôn ngữ bằng cách cung cấp định nghĩa về hành vi không xác định chính thức.

Hành vi xác định thực hiện cho phép người thực hiện tự do lựa chọn cách tiếp cận phù hợp, nhưng yêu cầu lựa chọn này phải được giải thích cho người dùng. Các hành vi được chỉ định là định nghĩa triển khai thường là những hành vi mà người dùng có thể đưa ra quyết định mã hóa có ý nghĩa dựa trên định nghĩa triển khai. Các nhà triển khai nên ghi nhớ tiêu chí này khi quyết định mức độ mở rộng của một định nghĩa triển khai. Như với hành vi không xác định, chỉ đơn giản là không dịch nguồn có chứa hành vi được xác định thực hiện không phải là một phản ứng thích hợp.

Quan trọng cũng là lợi ích cho các chương trình, không chỉ là lợi ích cho việc thực hiện. Một chương trình phụ thuộc vào hành vi không xác định vẫn có thể tuân thủ , nếu nó được chấp nhận bởi việc thực hiện tuân thủ. Sự tồn tại của hành vi không xác định cho phép chương trình sử dụng các tính năng không di động được đánh dấu rõ ràng như vậy ("hành vi không xác định"), mà không trở nên không tuân thủ. Các ghi chú hợp lý:

Mã C có thể không di động. Mặc dù nó cố gắng tạo cơ hội cho các lập trình viên viết các chương trình di động thực sự, Ủy ban không muốn buộc các lập trình viên phải viết một cách hợp lý, để ngăn chặn việc sử dụng C như một 'trình biên dịch cấp cao' ': khả năng viết cụ thể của máy mã là một trong những điểm mạnh của C. Chính nguyên tắc này phần lớn thúc đẩy việc phân biệt giữa chương trình tuân thủ nghiêm ngặtchương trình tuân thủ (§1.7).

Và tại 1.7 nó ghi chú

Định nghĩa ba lần về tuân thủ được sử dụng để mở rộng dân số của các chương trình tuân thủ và phân biệt giữa các chương trình tuân thủ bằng cách sử dụng một chương trình tuân thủ di động và thực hiện duy nhất.

Một chương trình tuân thủ nghiêm ngặt là một thuật ngữ khác cho một chương trình di động tối đa. Mục tiêu là cung cấp cho các lập trình viên một cơ hội chiến đấu để tạo ra các chương trình C mạnh mẽ cũng có tính di động cao, mà không làm giảm các chương trình C hoàn toàn hữu ích mà không thể mang theo được. Do đó trạng từ nghiêm chỉnh.

Do đó, chương trình bẩn nhỏ này hoạt động hoàn hảo trên GCC vẫn đang tuân thủ !


15

Điều tốc độ đặc biệt là một vấn đề khi so sánh với C. Nếu C ++ đã làm một số điều có thể hợp lý, như khởi tạo các mảng lớn của các kiểu nguyên thủy, nó sẽ mất một tấn điểm chuẩn cho mã C. Vì vậy, C ++ khởi tạo các kiểu dữ liệu của riêng nó, nhưng để lại các kiểu C theo cách chúng tồn tại.

Hành vi không xác định khác chỉ phản ánh thực tế. Một ví dụ là dịch chuyển bit với số lượng lớn hơn loại. Điều đó thực sự khác nhau giữa các thế hệ phần cứng của cùng một gia đình. Nếu bạn có ứng dụng 16 bit, cùng một nhị phân sẽ cho kết quả khác nhau trên 80286 và 80386. Vì vậy, tiêu chuẩn ngôn ngữ nói rằng chúng ta không biết!

Một số thứ chỉ được giữ nguyên như vậy, giống như thứ tự đánh giá các biểu hiện phụ không được chỉ định. Ban đầu điều này được cho là để giúp các nhà văn biên dịch tối ưu hóa tốt hơn. Ngày nay các trình biên dịch đủ tốt để tìm ra nó, nhưng chi phí tìm tất cả các vị trí trong các trình biên dịch hiện có tận dụng sự tự do là quá cao.


+1 cho đoạn thứ hai, cho thấy điều gì đó sẽ gây khó xử khi được chỉ định là hành vi được xác định theo thực hiện.
David Thornley

3
Sự thay đổi bit chỉ là một ví dụ về việc chấp nhận hành vi trình biên dịch không xác định và sử dụng các capabilites phần cứng. Sẽ là không đáng kể khi chỉ định kết quả C cho một chút thay đổi khi số lượng lớn hơn loại, nhưng tốn kém để thực hiện trên một số phần cứng.
mattnz

7

Như một ví dụ, truy cập con trỏ hầu như không được xác định và không nhất thiết chỉ vì lý do hiệu suất. Ví dụ, trên một số hệ thống, tải các thanh ghi cụ thể bằng một con trỏ sẽ tạo ra ngoại lệ phần cứng. Khi SPARC truy cập một đối tượng bộ nhớ được căn chỉnh không đúng sẽ gây ra lỗi bus, nhưng trên x86, nó sẽ "chậm". Thật khó để xác định hành vi trong những trường hợp đó vì phần cứng cơ bản chỉ ra điều gì sẽ xảy ra và C ++ có thể di chuyển được với rất nhiều loại phần cứng.

Tất nhiên nó cũng cho phép trình biên dịch tự do sử dụng kiến ​​thức cụ thể về kiến ​​trúc. Đối với một ví dụ hành vi không xác định, việc dịch chuyển đúng các giá trị đã ký có thể là logic hoặc số học tùy thuộc vào phần cứng cơ bản, để cho phép sử dụng bất kỳ thao tác dịch chuyển nào có sẵn và không bắt buộc mô phỏng phần mềm của nó.

Tôi cũng tin rằng nó làm cho công việc của trình biên dịch trở nên dễ dàng hơn nhưng tôi không thể nhớ lại ví dụ vừa rồi. Tôi sẽ thêm nó nếu tôi nhớ lại tình huống.


3
Ngôn ngữ C có thể đã được chỉ định sao cho nó luôn phải sử dụng các lần đọc từng byte trên các hệ thống có các hạn chế căn chỉnh và do đó nó phải cung cấp các bẫy ngoại lệ với hành vi được xác định rõ để truy cập địa chỉ không hợp lệ. Nhưng tất nhiên tất cả điều này sẽ rất tốn kém (về kích thước mã, độ phức tạp và hiệu suất) và sẽ không mang lại lợi ích gì cho việc lành mạnh, mã chính xác.
R ..

6

Đơn giản: Tốc độ và tính di động. Nếu C ++ đảm bảo rằng bạn có một ngoại lệ khi bạn hủy tham chiếu một con trỏ không hợp lệ, thì nó sẽ không thể chuyển sang phần cứng nhúng. Nếu C ++ đảm bảo một số thứ khác như luôn luôn khởi tạo nguyên thủy, thì nó sẽ chậm hơn và trong thời điểm xuất phát của C ++, chậm hơn là một điều thực sự, thực sự tồi tệ.


1
Huh? Các ngoại lệ phải làm gì với phần cứng nhúng?
Mason Wheeler

2
Các ngoại lệ có thể khóa hệ thống theo những cách rất tệ đối với Hệ thống nhúng cần phản hồi nhanh. Có những tình huống đọc sai ít gây hại hơn nhiều khi hệ thống bị chậm.
Kỹ sư thế giới

1
@Mason: Vì phần cứng phải bắt được quyền truy cập không hợp lệ. Windows dễ dàng vi phạm quyền truy cập và phần cứng nhúng không có hệ điều hành để làm bất cứ điều gì ngoại trừ chết.
DeadMG

3
Cũng nên nhớ rằng không phải CPU nào cũng có MMU để bảo vệ chống lại truy cập không hợp lệ trong phần cứng. Nếu bạn bắt đầu yêu cầu ngôn ngữ của mình kiểm tra tất cả các truy cập con trỏ, thì bạn phải mô phỏng MMU trên CPU mà không cần một - và do đó MỌI quyền truy cập bộ nhớ trở nên cực kỳ tốn kém.
fluffy

4

C đã được phát minh trên một máy có byte 9 bit và không có đơn vị dấu phẩy động - giả sử rằng nó đã bắt buộc các byte phải là 9 bit, từ 18 bit và phao có nên được thực hiện bằng cách sử dụng chuẩn IEEE754 không?


5
Tôi nghi ngờ bạn đang nghĩ về Unix - C ban đầu được sử dụng trên PDP-11, đây thực sự là các tiêu chuẩn hiện tại khá thông thường. Tôi nghĩ rằng ý tưởng cơ bản đứng dù sao.
Jerry Coffin

@Jerry - vâng, bạn nói đúng - Tôi đang già đi!
Martin Beckett

Yup - xảy ra với những người tốt nhất trong chúng ta, tôi sợ.
Jerry Coffin

4

Tôi không nghĩ lý do đầu tiên của UB là để cho trình biên dịch tối ưu hóa, nhưng chỉ có khả năng sử dụng triển khai rõ ràng cho các mục tiêu tại thời điểm khi các kiến ​​trúc đã đa dạng hơn bây giờ (hãy nhớ nếu C được thiết kế trên một PDP-11 có kiến ​​trúc hơi quen thuộc, cổng đầu tiên là Honeywell 635 ít quen thuộc hơn - địa chỉ từ, sử dụng từ 36 bit, byte 6 hoặc 9 bit, địa chỉ 18 bit ... ít nhất là nó sử dụng 2 bổ sung). Nhưng nếu tối ưu hóa nặng không phải là mục tiêu, thì việc triển khai rõ ràng không bao gồm thêm kiểm tra thời gian chạy cho tràn, số lượng ca trên kích thước đăng ký, bí danh trong các biểu thức sửa đổi nhiều giá trị.

Một điều khác được tính đến là dễ thực hiện. Trình biên dịch AC tại thời điểm đó là nhiều lượt sử dụng nhiều tiến trình vì có một tiến trình xử lý mọi thứ sẽ không thể thực hiện được (chương trình sẽ quá lớn). Yêu cầu kiểm tra mạch lạc nặng đã được thực hiện - đặc biệt là khi nó liên quan đến một số CU. (Một chương trình khác ngoài trình biên dịch C, lint, đã được sử dụng cho điều đó).


Tôi tự hỏi điều gì đã thúc đẩy triết lý thay đổi của UB từ "Cho phép lập trình viên sử dụng các hành vi được thể hiện bởi nền tảng của họ" đến "Tìm lý do để cho phép trình biên dịch thực hiện hành vi hoàn toàn lập dị"? Tôi cũng tự hỏi bao nhiêu tối ưu hóa như vậy cuối cùng cải thiện kích thước mã sau khi mã được sửa đổi để làm việc theo trình biên dịch mới? Tôi sẽ không ngạc nhiên nếu trong nhiều trường hợp, tác dụng duy nhất của việc thêm "tối ưu hóa" như vậy vào trình biên dịch là buộc các lập trình viên viết mã lớn hơn và chậm hơn để tránh trình biên dịch phá vỡ nó.
supercat

Đó là một sự trôi dạt trong POV. Mọi người trở nên ít biết về máy mà chương trình của họ chạy, họ trở nên quan tâm hơn đến tính di động nên họ tránh tùy thuộc vào hành vi được xác định, không xác định và thực hiện được xác định. Có áp lực đối với các trình tối ưu hóa để có kết quả tốt nhất trên điểm chuẩn, và điều đó có nghĩa là tận dụng mọi sự khoan hồng còn lại của thông số kỹ thuật. Cũng có một thực tế là Internet - Usenet tại một thời điểm, SE ngày nay - các luật sư ngôn ngữ cũng có xu hướng đưa ra một cái nhìn thiên vị về lý do cơ bản và hành vi của các nhà văn biên dịch.
AProgrammer

1
Điều tôi cảm thấy tò mò là những tuyên bố mà tôi đã thấy về tác động của "C giả định rằng các lập trình viên sẽ không bao giờ tham gia vào hành vi không xác định" - một thực tế trong lịch sử không phải là sự thật. Một tuyên bố đúng sẽ là "C cho rằng các lập trình viên sẽ không kích hoạt hành vi không được xác định bởi tiêu chuẩn trừ khi chuẩn bị xử lý hậu quả nền tảng tự nhiên của hành vi đó. Cho rằng C được thiết kế như một ngôn ngữ lập trình hệ thống, một phần lớn của mục đích của nó là để cho phép các lập trình viên làm những việc cụ thể theo hệ thống không được xác định bởi tiêu chuẩn ngôn ngữ, ý tưởng mà họ sẽ không bao giờ làm như vậy là vô lý.
supercat

Thật tốt cho các lập trình viên phải nỗ lực nhiều hơn để đảm bảo tính di động trong trường hợp các nền tảng khác nhau vốn đã làm những việc khác nhau , nhưng các nhà văn trình biên dịch lãng phí thời gian của mọi người khi họ loại bỏ các hành vi mà các lập trình viên trong lịch sử có thể dự kiến ​​là phổ biến đối với tất cả các trình biên dịch trong tương lai. Với các số nguyên in, như vậy n < INT_BITSi*(1<<n)sẽ không tràn, tôi sẽ xem xét i<<=n;để rõ ràng hơn i=(unsigned)i << n;; trên nhiều nền tảng, nó sẽ nhanh hơn và nhỏ hơn i*=(1<<N);. Những gì có được có trình biên dịch cấm nó?
supercat

Mặc dù tôi nghĩ rằng sẽ tốt cho tiêu chuẩn khi cho phép bẫy cho nhiều thứ mà nó gọi là UB (ví dụ như tràn số nguyên), và có những lý do chính đáng để nó không yêu cầu bẫy làm bất cứ điều gì có thể dự đoán được, tôi nghĩ rằng từ mọi quan điểm đều có thể tưởng tượng được tiêu chuẩn sẽ được cải thiện nếu yêu cầu hầu hết các hình thức của UB phải mang lại giá trị không xác định hoặc ghi lại thực tế rằng họ bảo lưu quyền làm một việc khác, mà không bắt buộc phải ghi lại những gì khác. Trình biên dịch tạo ra mọi thứ "UB" sẽ hợp pháp, nhưng có thể không hài lòng ...
supercat

3

Một trong những trường hợp cổ điển ban đầu đã được ký bổ sung số nguyên. Trên một số bộ xử lý đang sử dụng, điều đó sẽ gây ra lỗi và đối với các bộ xử lý khác, nó sẽ tiếp tục với một giá trị (có thể là giá trị mô-đun thích hợp). Chỉ định một trong hai trường hợp có nghĩa là các chương trình cho các máy có kiểu số học không thuận lợi sẽ phải có thêm mã, bao gồm một nhánh có điều kiện, cho một cái gì đó tương tự như phép cộng số nguyên.


Ngoài ra số nguyên là một trường hợp thú vị; ngoài khả năng hành vi bẫy mà trong một số trường hợp sẽ hữu ích nhưng trong các trường hợp khác có thể gây ra việc thực thi mã ngẫu nhiên, có những tình huống sẽ hợp lý khi trình biên dịch đưa ra suy luận dựa trên thực tế là tràn số nguyên không được chỉ định để bọc. Ví dụ, một trình biên dịch có int16 bit và các ca làm việc mở rộng ký hiệu đắt tiền có thể tính toán (uchar1*uchar2) >> 4bằng cách sử dụng một ca làm việc không có dấu hiệu mở rộng. Thật không may, một số trình biên dịch mở rộng các suy luận không chỉ cho kết quả, mà còn cho các toán hạng.
supercat

2

Tôi muốn nói rằng nó ít về triết học hơn là về thực tế - C luôn là ngôn ngữ đa nền tảng và tiêu chuẩn phải phản ánh điều đó và thực tế là tại thời điểm bất kỳ tiêu chuẩn nào được đưa ra, sẽ có một số lượng lớn các triển khai trên rất nhiều phần cứng khác nhau. Một tiêu chuẩn cấm hành vi cần thiết sẽ bị coi thường hoặc tạo ra một cơ quan tiêu chuẩn cạnh tranh.


Ban đầu, nhiều hành vi không được xác định để cho phép khả năng các hệ thống khác nhau sẽ làm những việc khác nhau, bao gồm kích hoạt bẫy phần cứng với trình xử lý có thể hoặc không thể định cấu hình (và có thể, nếu không được định cấu hình, gây ra hành vi không thể đoán trước). Chẳng hạn, yêu cầu dịch chuyển trái của giá trị âm không bị bẫy, sẽ phá vỡ bất kỳ mã nào được thiết kế cho một hệ thống nơi nó đã làm và dựa vào hành vi đó. Nói tóm lại, chúng không được xác định để không ngăn người thực hiện cung cấp các hành vi mà họ cho là hữu ích .
supercat

Tuy nhiên, thật không may, điều đó đã bị vặn vẹo đến mức ngay cả mã biết rằng nó chạy trên bộ xử lý sẽ làm điều gì đó hữu ích trong trường hợp cụ thể không thể tận dụng hành vi đó, bởi vì trình biên dịch có thể sử dụng thực tế là tiêu chuẩn C không 't chỉ định hành vi (mặc dù nền tảng sẽ) để áp dụng cách viết lại thế giới kỳ quái cho mã.
supercat

1

Một số hành vi không thể được xác định bởi bất kỳ phương tiện hợp lý. Tôi có nghĩa là truy cập một con trỏ bị xóa. Cách duy nhất để phát hiện nó sẽ là cấm giá trị con trỏ sau khi xóa (ghi nhớ giá trị của nó ở đâu đó và không cho phép bất kỳ hàm phân bổ nào trả về nó nữa). Không chỉ việc ghi nhớ như vậy sẽ là quá mức cần thiết, mà trong một chương trình chạy dài sẽ gây ra việc hết các giá trị con trỏ được phép.


hoặc bạn có thể phân bổ tất cả các con trỏ như weak_ptrvà vô hiệu hóa tất cả các tham chiếu đến một con trỏ nhận được delete... oh chờ đã, chúng tôi đang tiếp cận bộ sưu tập rác: /
Matthieu M.

boost::weak_ptrViệc triển khai là một mẫu khá tốt để bắt đầu cho mẫu sử dụng này. Thay vì theo dõi và vô hiệu hóa weak_ptrsbên ngoài, một weak_ptrchỉ đóng góp vào shared_ptrsố lượng yếu và số lượng yếu về cơ bản là một số liệu cho chính con trỏ. Vì vậy, bạn có thể vô hiệu hóa shared_ptrmà không cần phải xóa nó ngay lập tức. Nó không hoàn hảo (bạn vẫn có thể có rất nhiều weak_ptrs hết hạn duy trì cơ sở shared_countmà không có lý do chính đáng) nhưng ít nhất nó nhanh và hiệu quả.
fluffy

0

Tôi sẽ cho bạn một ví dụ trong đó có khá nhiều sự lựa chọn hợp lý ngoài hành vi không xác định. Về nguyên tắc, bất kỳ con trỏ nào cũng có thể trỏ đến bộ nhớ chứa bất kỳ biến nào, ngoại trừ các biến cục bộ mà trình biên dịch có thể biết chưa bao giờ lấy địa chỉ của chúng. Tuy nhiên, để có được hiệu năng chấp nhận được trên CPU hiện đại, trình biên dịch phải sao chép các giá trị biến vào các thanh ghi. Hoạt động hoàn toàn ra khỏi bộ nhớ là không khởi động.

Điều này về cơ bản cung cấp cho bạn hai sự lựa chọn:

1) Xóa mọi thứ khỏi thanh ghi trước khi truy cập thông qua một con trỏ, chỉ trong trường hợp con trỏ trỏ đến bộ nhớ của biến đó. Sau đó tải mọi thứ cần thiết trở lại vào thanh ghi, chỉ trong trường hợp các giá trị được thay đổi thông qua con trỏ.

2) Có một bộ quy tắc khi con trỏ được phép đặt bí danh cho một biến và khi trình biên dịch được phép giả định rằng con trỏ không bí danh một biến.

C opts cho tùy chọn 2, bởi vì 1 sẽ là khủng khiếp cho hiệu suất. Nhưng sau đó, điều gì xảy ra nếu một con trỏ bí danh một biến theo cách mà quy tắc C cấm? Vì hiệu ứng phụ thuộc vào việc trình biên dịch có thực sự lưu trữ biến trong một thanh ghi hay không, không có cách nào để chuẩn C đảm bảo chắc chắn các kết quả cụ thể.


Sẽ có một sự khác biệt về ngữ nghĩa giữa việc nói "Trình biên dịch được phép hành xử như thể X là đúng" và nói "Bất kỳ chương trình nào mà X không đúng sẽ tham gia vào Hành vi không xác định", mặc dù không may, các tiêu chuẩn không làm rõ sự khác biệt. Trong nhiều tình huống, bao gồm cả ví dụ bí danh của bạn, câu lệnh trước sẽ cho phép nhiều tối ưu hóa trình biên dịch mà không thể nào khác được; cái sau cho phép thêm một số "tối ưu hóa", nhưng nhiều tối ưu hóa sau là thứ mà các lập trình viên không muốn.
supercat

Ví dụ: nếu một số mã đặt a foothành 42 và sau đó gọi một phương thức sử dụng một con trỏ được sửa đổi bất hợp pháp để đặt foothành 44, tôi có thể thấy lợi ích khi nói rằng cho đến khi viết "hợp pháp" tiếp theo foo, cố gắng đọc nó có thể hợp pháp mang lại 42 hoặc 44 và một biểu thức như foo+foothậm chí có thể mang lại 86, nhưng tôi thấy ít lợi ích hơn nhiều khi cho phép trình biên dịch thực hiện các suy luận mở rộng và thậm chí hồi tố, thay đổi Hành vi không xác định mà tất cả các hành vi "tự nhiên" hợp lý của nó đều là lành tính, thành một giấy phép để tạo mã vô nghĩa.
supercat

0

Trong lịch sử, Hành vi không xác định có hai mục đích chính:

  1. Để tránh yêu cầu các tác giả biên dịch tạo mã để xử lý các điều kiện không bao giờ xảy ra.

  2. Để cho phép khả năng trong trường hợp không có mã để xử lý rõ ràng các điều kiện như vậy, việc triển khai có thể có nhiều loại hành vi "tự nhiên", trong một số trường hợp, sẽ hữu ích.

Một ví dụ đơn giản, trên một số nền tảng phần cứng, cố gắng cộng hai số nguyên có dấu dương có tổng quá lớn để khớp với một số nguyên đã ký sẽ mang lại một số nguyên âm được ký cụ thể. Trên các triển khai khác, nó sẽ kích hoạt bẫy bộ xử lý. Để tiêu chuẩn C bắt buộc một trong hai hành vi sẽ yêu cầu trình biên dịch cho các nền tảng có hành vi tự nhiên khác với tiêu chuẩn sẽ phải tạo thêm mã để mang lại hành vi chính xác - mã có thể đắt hơn mã để thực hiện bổ sung thực tế. Tồi tệ hơn, điều đó có nghĩa là các lập trình viên muốn có hành vi "tự nhiên" sẽ phải thêm nhiều mã hơn nữa để đạt được nó (và mã bổ sung đó sẽ lại đắt hơn so với bổ sung).

Thật không may, một số tác giả biên dịch đã đưa ra triết lý rằng các trình biên dịch nên tìm cách tạo ra các điều kiện gợi lên Hành vi không xác định và, cho rằng các tình huống như vậy có thể không bao giờ xảy ra, rút ​​ra những suy luận mở rộng từ đó. Do đó, trên một hệ thống có 32 bit int, mã đã cho như:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

tiêu chuẩn C sẽ cho phép trình biên dịch nói rằng nếu q lớn hơn 46341 hoặc lớn hơn, biểu thức q * q sẽ mang lại kết quả quá lớn để phù hợp với int, do đó gây ra Hành vi không xác định và do đó, trình biên dịch sẽ có quyền cho rằng không thể xảy ra và do đó sẽ không được yêu cầu tăng *pnếu nó xảy ra. Nếu mã gọi sử dụng *pnhư một chỉ báo cần loại bỏ kết quả tính toán, thì hiệu quả của việc tối ưu hóa có thể là lấy mã mang lại kết quả hợp lý trên các hệ thống thực hiện theo hầu hết mọi cách có thể tưởng tượng được với tràn số nguyên (bẫy có thể là xấu, nhưng ít nhất sẽ hợp lý), và biến nó thành mã có thể hành xử vô nghĩa.


-6

Hiệu quả là lý do thông thường, nhưng bất kể lý do gì, hành vi không xác định là một ý tưởng khủng khiếp cho tính di động. Trong thực tế, các hành vi không xác định trở thành không được xác minh, các giả định không có căn cứ.


7
OP đã chỉ định điều này: "Câu hỏi của tôi không phải là về hành vi không xác định là gì, hay nó thực sự xấu. Tôi biết những nguy hiểm và hầu hết các trích dẫn hành vi không xác định có liên quan từ tiêu chuẩn, vì vậy vui lòng không đăng câu trả lời về mức độ xấu của nó . " Có vẻ như bạn đã không đọc câu hỏi.
Etienne de Martel
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.