정의되지 않은 행동의 철학 여기에 항상 같은 질문을 계속하는

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

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

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

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

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



답변

먼저, 여기서는 “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는 “완전한”대안을 선택했다. 물론 이것은 매우 거친 비유 입니다. 어떤 경우 에도 내부 일관성 또는 완전성에 대한 공식적인 증거를 시도하는 사람은 아무도 없습니다 . 그럼에도 불구하고 일반적인 개념은 그들이 선택한 선택 에 상당히 밀접하게 부합 합니다 .


답변

C의 이론적 근거를 설명

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

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

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

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

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

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

그리고 1.7에

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

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

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


답변

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

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

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


답변

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

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

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


답변

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


답변

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


답변

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

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