정의되지 않은 행동의 철학


59

C \ C ++ 사양은 컴파일러가 자신의 방식으로 구현할 수있는 많은 수의 동작을 생략합니다. 여기에 항상 같은 질문을 계속하는 많은 질문이 있으며 이에 대한 훌륭한 게시물이 있습니다.

내 질문은 정의되지 않은 동작이 무엇인지에 대한 것이 아니거나 실제로 나쁜 것입니다. 나는 표준과 관련된 위험과 정의되지 않은 행동 따옴표의 대부분을 알고 있으므로 그것이 얼마나 나쁜지에 대한 답변을 게시하지 마십시오. 이 질문은 컴파일러 구현을 위해 너무 많은 동작을 열어 두는 철학에 관한 것입니다.

나는 성능이 주된 이유 라는 훌륭한 블로그 게시물 을 읽었습니다 . 성능이 그것을 허용하는 유일한 기준인지 궁금하거나 컴파일러 구현을 위해 열어두기로 결정하는 데 영향을 미치는 다른 요인이 있습니까?

정의되지 않은 특정 동작이 컴파일러가 최적화 할 수있는 충분한 공간을 제공하는 방법에 대해 언급 할 예가있는 경우이를 나열하십시오. 성능 이외의 다른 요소를 알고 있다면 충분히 자세하게 답변하십시오.

질문을 이해하지 못하거나 답변을 뒷받침 할 충분한 증거 / 자료가없는 경우 광범위하게 추측하는 답변을 게시하지 마십시오.


7
어쨌든 결정 론적 컴퓨터에 대해 들어 본 적이 있습니까?
sova

1
litb의 우수한 응답으로 programmers.stackexchange.com/a/99741/192238가 나타냅니다,이 질문의 제목과 몸이 일치하지 않는 조금 보인다 "자신의 방식으로 구현하는 컴파일러 오픈 행동을"보통이라고합니다 구현 정의 . 물론 실제 UB는 구현 작성자가 정의 할 수 있지만, 종종 UB는 귀찮게하지 않으며 (또는 최적화 등)
underscore_d

답변:


49

먼저, 여기서는 "C"만 언급하지만 C ++에도 동일하게 적용됩니다.

Godel을 언급 한 의견은 부분적으로 (그러나 부분적으로 만) 적절했습니다.

당신이 그것을 내려 할 때, C 표준에 정의되지 않은 동작입니다 주로 단지 표준 시도가 정의하는 것 사이의 경계를 지적하고, 무엇을하지 않습니다.

고델의 정리 (2 개가 있음)는 기본적으로 완전하고 일관된 것으로 입증 될 수있는 수학 시스템을 정의하는 것은 불가능하다고 기본적으로 말합니다. 규칙을 완성 할 수 있도록 (그가 다루는 경우는 자연수에 대한 "일반적인"규칙), 그렇지 않으면 일관성을 증명할 수는 있지만 둘 다 가질 수는 없습니다.

C와 같은 경우에는 직접 적용되지 않습니다. 대부분의 경우 시스템의 완전성 또는 일관성에 대한 "확장 성"은 대부분의 언어 설계자에게 우선 순위가 아닙니다. 동시에 그렇습니다. 아마도 "완벽한"시스템을 정의하는 것이 불가능하다는 것을 알면서도 (적어도 어느 정도) 영향을 받았을 것입니다. 그러한 일이 불가능하다는 것을 알면 뒤로 물러서서 조금 숨을 쉬고 그들이 정의하려는 대상의 경계를 결정하기가 조금 쉬워 졌을 수 있습니다.

오만 혐의로 기소 될 위험이 있지만, 나는 C 표준이 두 가지 기본 아이디어에 의해 지배되는 것으로 특징 지었다.

  1. 이 언어는 가능한 한 다양한 하드웨어를 지원해야합니다 (이상적으로는 모든 "정상적인"하드웨어를 합리적인 하한값으로 낮추십시오).
  2. 언어는 주어진 환경에 대해 가능한 한 다양한 소프트웨어 작성을 지원해야합니다.

첫 번째는 누군가가 새로운 CPU를 정의하는 경우 디자인이 최소한 몇 가지 간단한 지침에 가깝게 떨어지지 않는 한 C를 훌륭하고 견고하며 사용할 수 있도록 구현하는 것이 가능해야한다는 것을 의미합니다. Von Neumann 모델의 일반적인 순서를 따르고 C 구현을 허용하기에 충분한 최소한의 메모리를 제공합니다. "호스트 된"구현 (OS에서 실행되는 구현)의 경우 파일과 합리적으로 일치하는 특정 개념을 지원해야하며 특정 최소 문자 집합 (91이 필요함)을 갖는 문자 집합이 있어야합니다.

두 번째는 하드웨어를 직접 조작하는 코드를 작성하는 것이 가능해야 의미, 그래서 궁극적으로 있습니다 당신은 부트 로더, 운영체제 등 어떤 OS없이 실행되는 임베디드 소프트웨어, 같은 것을 쓸 수 있습니다 어떤 이 점에서 한계가 있으므로 거의 모든 실제 운영 체제, 부트 로더 등에 는 어셈블리 언어로 작성된 코드가 약간 포함되어있을 수 있습니다 . 마찬가지로, 작은 임베디드 시스템이라도 호스트 시스템의 장치에 액세스 할 수 있도록 사전 작성된 라이브러리 루틴이 적어도 포함되어있을 가능성이 있습니다. 정확한 경계를 정의하기는 어렵지만 그러한 코드에 대한 종속성을 최소화해야합니다.

언어에서 정의되지 않은 동작은 주로 언어가 이러한 기능을 지원하려는 의도에 의해 결정됩니다. 예를 들어, 언어를 사용하면 임의의 정수를 포인터로 변환하고 해당 주소에있는 모든 것에 액세스 할 수 있습니다. 표준은 수행 할 때 어떤 일이 일어날 지 말하려고 시도하지 않습니다 (예를 들어, 일부 주소에서 읽는 경우에도 외부에 영향을 줄 수 있음). 동시에, C로 작성할 수있는 어떤 종류의 소프트웨어 가 필요 하기 때문에 그러한 작업을 수행하지 못하게하려고 시도하지 않습니다 .

다른 디자인 요소로 인해 정의되지 않은 동작이 있습니다. 예를 들어 C의 다른 의도는 별도의 컴파일을 지원하는 것입니다. 이것은 (예를 들어) 우리 대부분이 일반적인 링커 모델로 보는 것과 거의 비슷한 링커를 사용하여 조각을 "링크"할 수 있음을 의미합니다. 특히 언어의 의미에 대한 지식없이 별도의 컴파일 된 모듈을 완전한 프로그램으로 결합 할 수 있어야합니다.

컴파일러 기술의 한계로 인해 존재하는 또 다른 유형의 정의되지 않은 동작 (C보다 C ++에서 더 일반적 임)이 있습니다. 기본적으로 우리가 알고있는 것은 오류이며 컴파일러가 오류로 진단하기를 원할 것입니다. 그러나 컴파일러 기술에 대한 현재의 한계를 감안할 때 모든 상황에서 진단이 가능하다는 것은 의심의 여지가 있습니다. 이들 중 다수는 별도의 편집과 같은 다른 요구 사항에 의해 결정되므로 충돌 요구 사항의 균형을 맞추는 문제가되며,이 경우위원회는 일반적으로 가능한 문제를 진단 할 수없는 경우에도 더 큰 기능을 지원하기로 결정했습니다. 가능한 모든 문제를 진단 할 수있는 기능을 제한하는 대신.

이러한 의도 의 차이는 C와 Java 또는 Microsoft의 CLI 기반 시스템과 같은 차이점을 대부분 이끌어냅니다. 후자는 훨씬 더 제한된 하드웨어 세트로 작업하거나 소프트웨어가 대상으로하는 더 구체적인 하드웨어를 에뮬레이트하도록 요구하는 것으로 상당히 명시 적으로 제한됩니다. 또한 하드웨어의 직접적인 조작 을 방지 하기 위해 JNI 또는 P / Invoke (및 C와 같이 작성된 코드)와 같은 것을 사용하여 그러한 시도를하도록 요구합니다.

잠시 고델의 정리로 돌아가서, 우리는 비슷한 것을 그릴 수있다. 자바와 CLI는 "내부적으로 일관성있는"대안을 선택했고 C는 "완전한"대안을 선택했다. 물론 이것은 매우 거친 비유 입니다. 어떤 경우 에도 내부 일관성 또는 완전성에 대한 공식적인 증거를 시도하는 사람은 아무도 없습니다 . 그럼에도 불구하고 일반적인 개념은 그들이 선택한 선택 에 상당히 밀접하게 부합 합니다 .


25
고델의 정리는 붉은 청어라고 생각합니다. C는 C로 지정할 필요가 없습니다. 튜링 머신을 고려하여 완전히 지정된 언어를 사용하는 것이 가능합니다.
poolie

9
미안하지만, 당신이 고델의 정리를 완전히 오해 한 것 같아요. 그들은 모든 논리적 인 진술을 일관된 논리 시스템으로 증명할 수없는 문제를 다룬다. 컴퓨팅 측면에서 불완전 성 정리는 어떤 프로그램으로도 해결할 수없는 문제가 있다고 말하는 것과 유사합니다. 문제는 실제 진술, 프로그램은 증명, 논리 시스템에 대한 계산 모델과 유사합니다. 그것은 정의되지 않은 행동과 전혀 관련이 없습니다. scottaaronson.com/blog/?p=710 에서 유추에 대한 설명을 참조하십시오 .
Alex ten Brink

5
C 구현에는 Von Neumann 시스템이 필요하지 않습니다. 하버드 아키텍처를위한 C 구현을 개발하는 것은 가능하며 (매우 어렵지는 않지만) 임베디드 시스템에서 그러한 구현을 많이보고있는 것에 놀라지 않을 것입니다.
bdonlan

1
불행히도 현대 C 컴파일러 철학은 UB를 완전히 새로운 수준으로 끌어 올립니다. 프로그램이 특정 형태의 정의되지 않은 행동으로 인한 거의 모든 그럴듯한 "자연적인"결과를 처리 할 준비가되어 있고 처리 할 수 ​​없었던 결과가 적어도 인식 가능할 경우 (예 : 갇힌 정수 오버 플로우) 새로운 철학은 유리합니다. UB가 발생하지 않는 한 실행할 수없는 코드를 우회하여 대부분의 구현에서 올바르게 동작하는 코드를 "보다 효율적"이지만 명백히 잘못된 코드로 바꿉니다.
supercat

20

C의 이론적 근거를 설명

지정되지 않은 동작, 정의되지 않은 동작 및 구현 정의 된 동작이라는 용어는 표준 속성이 완전히 설명 할 수 없거나 완전히 설명 할 수없는 프로그램 작성 결과를 분류하는 데 사용됩니다. 이 분류를 채택하는 목적은 표준에 따른 적합성을 제거하지 않고도 구현 품질을 시장에서 적극적으로 강화할 수있을뿐만 아니라 시장에서 활발한 힘을 발휘할 수 있도록하는 다양한 구현을 허용하는 것 입니다. 표준에 대한 부록 F는이 세 가지 범주 중 하나에 해당하는 행동을 정리합니다.

지정되지 않은 동작은 구현 자에게 프로그램을 번역 할 때 위도를 제공합니다. 이 위도는 프로그램을 번역하지 못하는 한 확장되지 않습니다.

정의되지 않은 동작은 구현 자에게 진단하기 어려운 특정 프로그램 오류를 포착하지 못하도록 라이센스를 부여합니다. 또한 가능한 언어 확장이 가능한 영역을 식별합니다. 구현자는 공식적으로 정의되지 않은 동작의 정의를 제공하여 언어를 보강 할 수 있습니다.

구현 정의 동작은 구현 자에게 적절한 접근 방식을 선택할 수있는 자유를 제공하지만이 선택 사항을 사용자에게 설명해야합니다. 구현 정의로 지정된 동작은 일반적으로 사용자가 구현 정의를 기반으로 의미있는 코딩 결정을 내릴 수있는 동작입니다. 구현자는 구현 정의의 범위를 결정할 때이 기준을 명심해야합니다. 지정되지 않은 동작과 마찬가지로 구현 정의 동작을 포함하는 소스를 변환하지 못하는 것만으로는 적절하지 않습니다.

구현의 이점뿐만 아니라 프로그램의 이점도 중요합니다. 정의되지 않은 동작에 의존하는 프로그램 은 준수 구현에 의해 승인 된 경우 에도 여전히 준수 할 수 있습니다 . 정의되지 않은 동작이 존재하면 프로그램은 부적합하지 않고 명시 적으로 표시 할 수없는 이식 가능 기능 ( "정의되지 않은 동작")을 사용할 수 있습니다. 이론적 근거 :

C 코드는 이식 할 수 없습니다. 프로그래머가 실제로 이식 가능한 프로그램을 작성할 수있는 기회를 제공하기 위해 노력했지만위원회는 프로그래머를 강제로 작성하여 C를``고수준 어셈블러 ''로 사용하는 것을 배제하기를 원하지 않았습니다. 코드는 C의 강점 중 하나입니다.이 원칙은 엄격하게 준수하는 프로그램일치하는 프로그램 (§1.7)을 구분하는 데 큰 동기를 부여합니다 .

그리고 1.7에

준수의 3 중 정의는 준수 ​​프로그램의 인구를 확대하고 단일 구현 및 휴대용 준수 프로그램을 사용하는 준수 프로그램을 구별하는 데 사용됩니다.

엄격하게 준수하는 프로그램은 최대한 이식 가능한 프로그램의 다른 용어입니다. 목표는 프로그래머가 이식성이없는 강력한 C 프로그램을 손상시키지 않으면 서 이식성이 뛰어난 강력한 C 프로그램을 만들 수있는 기회를 제공하는 것입니다. 따라서 부사는 엄격하게.

따라서 GCC에서 완벽하게 작동하는이 더러운 프로그램은 여전히 적합합니다 !


15

C와 비교할 때 속도 문제는 특히 문제가됩니다. C ++에서 기본 유형의 큰 배열을 초기화하는 것과 같이 합리적 일 수있는 몇 가지 작업을 수행하면 C 코드에 대한 수많은 벤치 마크가 손실됩니다. 따라서 C ++은 자체 데이터 형식을 초기화하지만 C 형식은 그대로 둡니다.

정의되지 않은 다른 행동은 현실을 반영합니다. 한 가지 예는 유형보다 카운트가 큰 비트 시프 팅입니다. 실제로 동일한 제품군의 하드웨어 세대마다 다릅니다. 16 비트 앱을 사용하는 경우 정확히 동일한 바이너리는 80286 및 80386에서 다른 결과를 제공합니다. 따라서 언어 표준에 따르면 우리는 모른다고합니다.

하위 표현식의 평가 순서가 지정되지 않은 것과 같은 일부 항목은 그대로 유지됩니다. 원래 이것은 컴파일러 작성자가 더 잘 최적화하는 데 도움이되는 것으로 생각되었습니다. 오늘날 컴파일러는 어쨌든 알아낼 정도로 충분하지만 기존 컴파일러에서 자유를 이용하는 모든 장소를 찾는 데 드는 비용은 너무 높습니다.


두 번째 단락에 +1. 구현 정의 동작으로 지정하기 어색한 것을 보여줍니다.
David Thornley

3
비트 시프트는 정의되지 않은 컴파일러 동작을 허용하고 하드웨어 기능을 사용하는 예일뿐입니다. 카운트가 유형보다 크지 만 일부 하드웨어에서 구현하는 데 비용이 많이 드는 경우 비트 시프트에 대한 C 결과를 지정하는 것은 쉽지 않습니다.
mattnz

7

일례로, 포인터 액세스는 성능상의 이유가 아니라 정의되지 않은 상태 여야합니다. 예를 들어, 일부 시스템에서는 포인터로 특정 레지스터를로드하면 하드웨어 예외가 발생합니다. SPARC에서 잘못 정렬 된 메모리 객체에 액세스하면 버스 오류가 발생하지만 x86에서는 "그냥"느려집니다. 기본 하드웨어가 어떤 일이 발생할지 결정하고 C ++은 많은 유형의 하드웨어에 이식 가능하므로 실제로 이러한 경우 동작을 지정하는 것은 까다 롭지 않습니다.

물론 컴파일러는 아키텍처 별 지식을 자유롭게 사용할 수 있습니다. 지정되지 않은 동작 예제의 경우, 서명 된 값의 오른쪽 이동은 기본 하드웨어에 따라 논리적이거나 산술적 일 수 있으므로 사용 가능한 이동 조작을 사용하고 소프트웨어 에뮬레이션을 강제하지 않는 것이 가능합니다.

또한 컴파일러 작성기의 작업이 훨씬 쉬워 지지만 지금은 예제를 기억할 수 없습니다. 상황을 기억하면 추가하겠습니다.


3
C 언어는 정렬 제한이있는 시스템에서 항상 바이트 단위 읽기를 사용해야하고 유효하지 않은 주소 액세스에 대해 잘 정의 된 동작을 가진 예외 트랩을 제공하도록 지정 될 수 있습니다. 그러나 물론이 모든 것은 (코드 크기, 복잡성 및 성능면에서) 엄청나게 많은 비용이 들었으며, 올바른 코드를 제정하는 데 아무런 이점을 제공하지 않았을 것입니다.
R ..

6

간단 함 : 속도 및 이식성. 유효하지 않은 포인터를 역 참조 할 때 C ++에서 예외가 발생했다는 것을 보증하면 임베디드 하드웨어로 이식 할 수 없습니다. 만약 C ++이 항상 초기화 된 프리미티브와 같은 다른 것들을 보장한다면, 느려질 것이고, C ++의 시작 시점에서 느리게는 정말, 정말 나쁜 것입니다.


1
응? 임베디드 하드웨어와 관련하여 예외는 무엇입니까?
메이슨 휠러

2
예외는 신속하게 대응해야하는 임베디드 시스템에 매우 나쁜 방식으로 시스템을 잠글 수 있습니다. 잘못된 판독으로 인해 시스템 속도가 느려지는 경우가 있습니다.
세계 엔지니어

1
@Mason : 하드웨어가 잘못된 액세스를 잡아야하기 때문입니다. Windows에서 액세스 위반이 발생하기 쉬우 며 운영 체제가없는 내장 하드웨어가 다이 이외의 작업을 수행하기가 더 어렵습니다.
DeadMG

3
또한 모든 CPU에 MMU가있는 것은 아니므로 처음부터 하드웨어의 잘못된 액세스로부터 보호해야합니다. 모든 포인터 액세스를 확인하기 위해 언어를 요구하기 시작하면 하나없이 CPU에서 MMU를 에뮬레이션해야하므로 모든 메모리 액세스가 매우 비쌉니다.
푹신한

4

C는 9 비트 바이트가 있고 부동 소수점 단위가없는 머신에서 발명되었습니다. 바이트가 9 비트, 워드 18 비트, 부동 소수점이 IEEE754 사전 방식을 사용하여 구현되어야한다고 가정 했습니까?


5
유닉스에 대해 생각하고 있다고 생각합니다 .C는 원래 PDP-11에서 사용되었으며 실제로는 기존의 표준입니다. 그럼에도 불구하고 나는 기본 아이디어가 있다고 생각합니다.
Jerry Coffin

@ 제리-네, 당신 말이 맞아요-나이가 들었어요!
Martin Beckett

그렇습니다. 우리 모두에게 일어날 일이 두렵습니다.
Jerry Coffin

4

UB의 첫 번째 이론적 근거는 컴파일러가 최적화 할 공간을 마련하는 것이 아니라 아키텍처가 현재보다 다양 할 때 타겟에 대해 명백한 구현을 사용할 가능성이라고 생각합니다 (C가 다소 친숙한 아키텍처를 가진 PDP-11은 허니 웰 635향했다. 허니 웰 635 는 36 비트 워드, 6 비트 또는 9 비트 바이트, 18 비트 주소를 사용하여 어드레싱 가능, 적어도 2를 사용했다. 보어). 그러나 엄격한 최적화가 목표가 아닌 경우 명백한 구현에는 오버플로, 레지스터 크기의 시프트 횟수에 대한 런타임 검사 추가, 여러 값을 수정하는 표현식의 별명 추가가 포함되지 않습니다.

고려해야 할 또 다른 것은 구현의 용이성이었습니다. 당시 AC 컴파일러는 하나의 프로세스로 모든 것이 가능하지 않았기 때문에 (프로세스가 너무 클 수 있기 때문에) 다중 프로세스를 사용하는 다중 패스였습니다. 강한 일관성 검사를 요구하는 것은 실패했습니다. 특히 CU가 여러 개인 경우에 그러했습니다. (C 컴파일러 이외의 다른 프로그램 인 lint가 사용되었습니다).


UB의 변화 철학이 "프로그래머가 플랫폼에 노출 된 동작을 사용하도록 허용"에서 "컴파일러가 완전히 이상한 동작을 구현할 수있는 변명 찾기"에서 UB의 변화 철학을 어떻게 이끌어 냈는지 궁금하십니까? 또한 새로운 컴파일러에서 작동하도록 코드를 수정 한 후 그러한 최적화로 인해 코드 크기가 얼마나 향상되는지 궁금합니다. 많은 경우에 컴파일러에 이러한 "최적화"를 추가하는 유일한 효과가 프로그래머가 더 크고 느린 코드를 작성하여 컴파일러가 중단되지 않도록하는 것이라면 놀라지 않을 것입니다.
supercat

POV의 드리프트입니다. 사람들은 자신의 프로그램이 실행되는 머신에 대해 잘 알지 못하고 이식성에 더 관심을 갖게되었으므로 정의되지 않은 지정되지 않은 구현 정의 동작에 따라 피했습니다. 벤치 마크에서 최상의 결과를 얻으려면 옵티 마이저에 압력이 가해졌으며 이는 언어 사양에 남은 모든 신뢰도를 사용한다는 의미입니다. 인터넷 (현재 유즈넷, 한 번에 SE)은 언어 변호사들도 컴파일러 작성자의 기본 이론적 근거와 행동에 대해 편견을 갖고있는 경향이 있다는 사실도 있습니다.
AProgrammer

1
제가 궁금한 점은 "C는 프로그래머가 정의되지 않은 행동을하지 않을 것이라고 가정합니다"라는 효과에 대해 언급 한 것입니다. 역사적으로 사실이 아닙니다. 원래 올바른 문장은 "했을 C는 처리 할 준비가되지 않는 한 프로그래머는 표준에 의해 정의되지 않은 동작을 트리거하지 않을 것이라고 가정 자연 플랫폼 결과 그 행동. C는 시스템 프로그래밍 언어, 그것의 큰 부분으로 디자인 된 점을 감안 목적 프로그래머가 언어 표준에 의해 정의되지 않은 시스템 특정 작업을 수행 할 수 있도록하는 것이 었습니다. 그렇게하지 않을 것이라는 생각은 터무니 없습니다
supercat

프로그래머가 다른 플랫폼이 본질적으로 다른 일을 할 경우 이식성을 확보하기 위해 추가 노력을 기울이는 것이 좋지만, 컴파일러 작성자는 프로그래머가 역사적으로 미래의 모든 컴파일러에 공통적으로 기대할 수있는 행동을 제거하면 모든 시간을 낭비합니다. 을 감안할 때 정수 in, 그와 같은 n < INT_BITSi*(1<<n)오버 플로우 않을 것이다, 나는 고려할 것 i<<=n;보다 더 명확하게하기 위해 i=(unsigned)i << n;; 많은 플랫폼에서보다 빠르고 작습니다 i*=(1<<N);. 컴파일러가 금지하는 것은 무엇입니까?
supercat

나는 표준이 UB라고 부르는 많은 것들에 대해 트랩을 허용하는 것이 좋을 것이라고 생각하지만 (예 : 정수 오버플로) 트랩이 예측 가능한 것을 수행하지 않아도되는 이유는 충분하지만 모든 관점에서 상상할 수 있다고 생각합니다. 대부분의 형태의 UB가 불확실한 가치를 산출하거나 다른 것을 수행 할 권리가 있음을 문서화 할 필요없이 다른 것을 할 권리를 보유한다는 사실을 문서화해야하는 경우 표준이 개선 될 것입니다. 모든 것을 "UB"로 만든 컴파일러는 합법적이지만 바람직하지 않을 것입니다.
supercat

3

초기 고전 사례 중 하나는 부호있는 정수 추가였습니다. 사용중인 일부 프로세서에서는 오류가 발생하고 다른 프로세서에서는 값 (예 : 적절한 모듈 식 값)으로 계속 진행됩니다. 두 경우 중 하나를 지정하면 바람직하지 않은 산술 스타일을 가진 기계 용 프로그램에는 정수 추가와 비슷한 조건부 분기를 포함하여 추가 코드가 있어야합니다.


정수 추가는 흥미로운 경우입니다. 어떤 경우에는 유용하지만 다른 경우에는 임의 코드 실행을 유발할 수있는 트랩 동작의 가능성을 넘어서서, 정수 오버 플로우가 랩핑되도록 지정되지 않았다는 사실에 기초하여 컴파일러가 추론하는 것이 합리적 일 수있는 상황이 있습니다. 예를 들어 int16 비트이고 부호 확장 시프트가 비싼 컴파일러 는 부호 확장 (uchar1*uchar2) >> 4되지 않은 시프트를 사용하여 계산할 수 있습니다. 불행하게도 일부 컴파일러는 결과뿐만 아니라 피연산자로 추론을 확장합니다.
supercat

2

나는 그것이 현실보다는 철학에 관한 것이 아니라고 말하고 싶습니다 .C는 항상 크로스 플랫폼 언어였습니다. 그리고 표준은 그것을 반영해야하며 표준이 릴리스 될 당시에는 많은 다른 하드웨어에서 많은 수의 구현. 필요한 행동을 금지하는 표준은 무시되거나 경쟁 표준기구를 생성합니다.


원래, 구성 가능하거나 구성 불가능한 (및 구성되지 않은 경우 임의로 예측할 수없는 동작을 유발할 수있는) 핸들러로 하드웨어 트랩을 트리거하는 등 다른 시스템이 다른 작업을 수행 할 가능성을 허용하기 위해 많은 동작이 정의되지 않은 상태로 남아있었습니다. 예를 들어, 음수 값의 왼쪽 이동이 트랩 되지 않도록 요구 하면 그러한 동작에 의존하고 의존하는 시스템을 위해 설계된 코드가 손상됩니다. 요컨대, 구현자가 자신이 유용하다고 생각한 행동을 제공하지 못하도록 정의되지 않은 채로 두었습니다 .
supercat

그러나 불행히도 특정 경우에 유용한 무언가를 수행하는 프로세서에서 실행되고 있음을 알고있는 코드조차도 그러한 동작을 활용할 수 없습니다. 컴파일러는 C 표준이 코드에 기괴한 세계 재 작성을 적용하는 동작 (플랫폼은 아니지만)을 지정하지 마십시오.
supercat

1

어떤 행동은 합리적인 수단으로 정의 될 수 없습니다. 삭제 된 포인터에 액세스하는 것을 의미합니다. 그것을 감지하는 유일한 방법은 삭제 후 포인터 값을 금지하는 것입니다 (어딘가에 값을 기억하고 할당 함수가 더 이상 반환하지 않도록). 이러한 암기뿐만 아니라 너무 오래 실행 프로그램에 대한 허용 포인터 값이 부족하게 될 것입니다.


또는 당신은 모든 포인터를 할당하고 d weak_ptr를 얻는 포인터에 대한 모든 참조를 무효화 할 수 있습니다 delete... 오 기다려, 우리는 가비지 수집에 접근하고 있습니다 : /
Matthieu M.

boost::weak_ptr의 구현은이 사용 패턴에서 시작하기에 매우 좋은 템플릿입니다. weak_ptrs외부를 추적하고 무효화하는 대신 , 약한 수에 weak_ptr기여 shared_ptr하고 약한 수는 기본적으로 포인터 자체에 대한 참조입니다. 따라서 shared_ptr즉시 삭제하지 않고도 무효화 할 수 있습니다 . 완벽하지는 않지만 (아직 좋은 이유없이 weak_ptr기본 shared_count을 유지 관리하는 만료 된 항목을 많이 가질 수는 있지만) 적어도 빠르고 효율적입니다.
푹신한

0

나는 정의되지 않은 행동 이외의 합리적인 선택이 거의없는 예를 보여줄 것입니다. 원칙적으로 컴파일러는 알 수없는 로컬 변수를 제외하고는 모든 변수를 포함하는 메모리를 가리키는 포인터를 사용할 수 있습니다. 그러나 최신 CPU에서 적절한 성능을 얻으려면 컴파일러에서 변수 값을 레지스터에 복사해야합니다. 메모리를 완전히 사용하지 않는 것은 스타터가 아닙니다.

이것은 기본적으로 두 가지 선택을 제공합니다.

1) 포인터가 특정 변수의 메모리를 가리키는 경우를 대비하여 포인터를 통해 액세스하기 전에 레지스터에서 모든 것을 플러시하십시오. 그런 다음 포인터를 통해 값이 변경된 경우를 대비하여 필요한 모든 것을 레지스터로 다시로드하십시오.

2) 포인터가 변수의 앨리어스를 허용 할 때와 컴파일러가 포인터가 변수를 앨리어싱하지 않는다고 가정 할 수있는 규칙 세트를 갖습니다.

C는 옵션 2를 선택합니다. 1은 성능이 끔찍하기 때문입니다. 그러나 C 규칙이 금지하는 방식으로 포인터가 변수의 별칭을 지정하면 어떻게됩니까? 결과는 컴파일러가 실제로 변수를 레지스터에 저장했는지 여부에 따라 달라 지므로 C 표준이 특정 결과를 확실하게 보장 할 수있는 방법은 없습니다.


"컴파일러는 X가 참인 것처럼 동작하도록 허용됩니다"와 "X가 참이 아닌 모든 프로그램은 정의되지 않은 동작에 관여 할 것"이라는 말과 의미 상 차이가 있지만, 불행히도 구별을 명확하게하지 않는 표준은 있습니다. 앨리어싱 예제를 포함한 많은 상황에서 앞의 문장은 그렇지 않으면 불가능한 많은 컴파일러 최적화를 허용합니다. 후자는 좀 더 "최적화"를 허용하지만 후자의 최적화는 프로그래머가 원하지 않는 것입니다.
supercat

예를 들어, 일부 코드가 a foo를 42로 설정 한 다음 불법적으로 수정 된 포인터를 사용 foo하여 44 로 설정하는 메소드를 호출하는 경우 다음 "정상적인"쓰기 때까지 foo읽을 때 합법적으로 읽을 수 있다는 이점 이 있습니다. yield 42 또는 44와 같은 식으로 foo+foo86을 산출 할 수도 있지만 컴파일러가 확장 가능하고 소급하여 추론 할 수있는 이점이 훨씬 적다는 것을 알 수 있습니다. 무의미한 코드를 생성합니다.
supercat

0

역사적으로 정의되지 않은 행동에는 두 가지 주요 목적이있었습니다.

  1. 컴파일러 작성자가 절대로 발생해서는 안되는 조건을 처리하는 코드를 생성하도록 요구하지 않습니다.

  2. 이러한 조건을 명시 적으로 처리 할 코드가 없을 경우 구현에는 여러 가지 "자연적인"동작이있을 수 있으며 경우에 따라 유용 할 수 있습니다.

간단한 예로, 일부 하드웨어 플랫폼에서 부호가있는 정수에 맞지 않는 합계가 너무 큰 양의 부호있는 정수를 더하려고하면 특정 음의 부호있는 정수가 생성됩니다. 다른 구현에서는 프로세서 트랩을 트리거합니다. C 표준이 동작하기 위해서는 두 가지 동작 모두 표준과는 다른 자연스러운 동작을 가진 플랫폼의 컴파일러가 정확한 동작을 생성하기 위해 추가 코드를 생성해야합니다. 실제 코드를 추가하는 데 코드보다 비쌀 수 있습니다. 더 나쁜 것은, "자연스러운"행동을 원했던 프로그래머는 그것을 달성하기 위해 더 많은 코드를 추가해야 할 것입니다 (그리고 추가 코드는 다시 추가하는 것보다 더 비쌉니다).

불행하게도, 일부 컴파일러 제작자들은 컴파일러가 정의되지 않은 동작을 유발하는 조건을 찾기 위해 자신의 길을 떠나야한다는 철학을 취했으며 그러한 상황이 결코 발생하지 않을 것이라고 가정하면 그로부터 연장 된 추론을 이끌어냅니다. 따라서 32 비트 시스템 int에서 다음과 같은 코드가 제공됩니다.

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

C 표준을 통해 컴파일러는 q가 46341 이상인 경우 q * q 표현식이 너무 커서 결과에 맞지 않아 int결과적으로 정의되지 않은 동작이 발생하고 결과적으로 컴파일러는 다음과 같이 가정 할 수 있습니다. 일어날 수 없으므로 증가 *p할 필요가 없습니다 . 호출 코드가 *p계산 결과를 버려야한다는 지표로 사용하는 경우, 최적화의 효과는 정수 오버플로로 상상할 수있는 거의 모든 방식으로 수행되는 시스템에서 합리적인 결과를 산출하는 코드를 취하는 것입니다 (트 랩핑은 추악하지만 적어도 합리적 일 것입니다.)


-6

효율성은 일반적인 변명이지만, 변명의 여하튼 정의되지 않은 행동은 이식성에 대한 끔찍한 아이디어입니다. 실제로 정의되지 않은 동작은 확인되지 않은 상태로 가정됩니다.


7
OP는 다음과 같이 지정했습니다. "제 질문은 정의되지 않은 행동이 무엇인지에 관한 것이거나 실제로 나쁜 것입니다. 나는 표준의 위험과 관련된 정의되지 않은 행동 따옴표 대부분을 알고 있으므로 그것이 얼마나 나쁜지에 대한 답변을 게시하지 마십시오. " 질문을 읽지 않은 것 같습니다.
Etienne de Martel
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.