정의되지 않은 동작을 호출하는 잘못 작성된 C ++ 소스 코드를 컴파일한다고 가정 해 보겠습니다.
C ++ 언어 사양이 "적합한"컴파일러에서 허용되는 것으로 간주하는 관점에서 볼 때이 시나리오에서 "무엇이든"수행하면 컴파일러 충돌 (또는 내 암호 도용 또는 컴파일 타임에 오작동 또는 오류 발생)이 포함됩니다. 정의되지 않은 동작의 범위는 결과 실행 파일이 실행될 때 발생할 수있는 일로 특별히 제한됩니까?
정의되지 않은 동작을 호출하는 잘못 작성된 C ++ 소스 코드를 컴파일한다고 가정 해 보겠습니다.
C ++ 언어 사양이 "적합한"컴파일러에서 허용되는 것으로 간주하는 관점에서 볼 때이 시나리오에서 "무엇이든"수행하면 컴파일러 충돌 (또는 내 암호 도용 또는 컴파일 타임에 오작동 또는 오류 발생)이 포함됩니다. 정의되지 않은 동작의 범위는 결과 실행 파일이 실행될 때 발생할 수있는 일로 특별히 제한됩니까?
답변:
정의되지 않은 동작의 규범 적 정의는 다음과 같습니다.
이 국제 표준이 요구 사항을 부과하지 않는 행동
[참고 :이 국제 표준이 행동에 대한 명시적인 정의를 생략하거나 프로그램이 잘못된 구성이나 잘못된 데이터를 사용하는 경우 정의되지 않은 동작이 예상 될 수 있습니다. 허용되지 않는 정의되지 않은 동작은 예측할 수없는 결과로 상황을 완전히 무시하는 것, 번역 또는 프로그램 실행 중에 환경 특성 (진단 메시지 발행 여부에 관계없이)의 문서화 된 방식으로 동작하는 것, 번역 또는 실행 종료 (발행 포함)에 이르기까지 다양합니다. 진단 메시지). 많은 잘못된 프로그램 구성은 정의되지 않은 동작을 발생시키지 않습니다. 그들은 진단을 받아야합니다. 상수 표현식의 평가는 정의되지 않은 것으로 명시 적으로 지정된 동작을 나타내지 않습니다. — 끝 참고]
메모 자체는 표준이 아니지만 구현으로 알려진 다양한 동작을 설명합니다. 따라서 컴파일러 충돌 (번역이 갑자기 종료 됨)은 해당 메모에 따르면 합법적입니다. 그러나 실제로 규범적인 텍스트가 말했듯이 표준은 실행이나 번역에 대한 경계를 두지 않습니다. 구현에서 암호를 훔친다고해서 표준에 명시된 계약 위반이 아닙니다.
NULL-deref 또는 0으로 나누기와 같이 우리가 일반적으로 걱정하는 대부분의 종류의 UB는 런타임 UB입니다. 실행 시 런타임 UB를 유발하는 함수를 컴파일 하면 컴파일러가 충돌하지 않아야합니다. 어쩌면하지 않는 한 그것은 기능 (및 기능을 통해 해당 경로)을 확실히 증명할 수있는 것입니다 프로그램에 의해 실행.
(두 번째 생각 : 아마도 컴파일 타임에 템플릿 / consistexpr에 필요한 평가를 고려하지 않았을 것입니다. 아마도 그 동안 UB는 결과 함수가 호출되지 않더라도 번역 중에 임의의 이상 함을 유발할 수 있습니다.)
번역하는 동안 행동하는 는 ISO C ++ 인용의 일부 이야기꾼의 대답 @에서는 ISO C 표준에서 사용되는 언어와 유사하다. C는 constexpr
컴파일 타임에 템플릿이나 필수 평가를 포함하지 않습니다 .
그러나 재미있는 사실 : ISO C는 번역이 종료되면 진단 메시지와 함께 있어야한다는 메모에서 말합니다. 또는 "번역 중 ... 문서화 된 방식으로 행동". 나는 "상황을 완전히 무시하는 것"이 번역 중단을 포함하는 것으로 읽을 수 있다고 생각하지 않습니다.
번역 시간 UB에 대해 배우기 전에 작성된 오래된 답변. 하지만 런타임 -UB의 경우에는 사실이므로 잠재적으로 여전히 유용합니다.
컴파일 타임에 발생 하는 UB 같은 것은 없습니다 . 그것은 일 수 볼 실행의 특정 경로를 따라 컴파일러 있지만, C ++ 용어는되지 일어난 실행에 도달 할 때까지 함수를 통해 실행 경로있다.
컴파일조차 불가능하게 만드는 프로그램의 결함은 UB가 아니라 구문 오류입니다. 이러한 프로그램은 C ++ 용어로 "잘 구성되지 않았습니다"(내 표준이 올바른 경우). 프로그램은 잘 구성 될 수 있지만 UB를 포함합니다. 정의되지 않은 동작과 잘못된 형식의 차이, 진단 메시지가 필요하지 않음
내가 뭔가를 오해하지 않는 한 ISO C ++ 에서는 이 프로그램이 올바르게 컴파일되고 실행되도록 요구 합니다. 실행은 0으로 나누기에 도달하지 않기 때문입니다. (실제로 ( Godbolt ) 좋은 컴파일러는 실행 가능한 파일을 만듭니다. gcc / clang은 x / 0
최적화 할 때도 경고 하지만 이것 에 대해서는 경고 하지 않습니다. 어쨌든 우리는 낮은 ISO C ++가 구현 품질을 허용 하는 정도 를 알려줍니다 . 따라서 gcc를 확인합니다. / clang은 내가 프로그램을 올바르게 작성했는지 확인하는 것 외에는 거의 유용한 테스트가 아닙니다.)
int cause_UB() {
int x=0;
return 1 / x; // UB if ever reached.
// Note I'm avoiding x/0 in case that counts as translation time UB.
// UB still obvious when optimizing across statements, though.
}
int main(){
if (0)
cause_UB();
}
이에 대한 유스 케이스는 C 전 처리기 또는 constexpr
변수 및 해당 변수에 대한 분기를 포함 할 수 있으며, 이는 이러한 상수 선택에 도달하지 않은 일부 경로에서 말도 안되는 결과를 초래합니다.
컴파일 타임에 보이는 UB를 유발하는 실행 경로는 절대 취하지 않는다고 가정 할 수 있습니다. 예를 들어 x86 용 컴파일러 ud2
는 cause_UB()
. 또는 함수 내에서 한 쪽이 증명 가능한 UB 로 if()
이어지는 경우 분기를 제거 할 수 있습니다.
그러나 컴파일러는 여전히 모든 컴파일하는 다른 건전하고 올바른 방법입니다. 모든 경로 하지 않는 만남 (또는 만남을 입증 할 수 없음) UB는 여전히 실행하는대로-경우 C ++ 추상 기계가 실행되었다 ASM로 컴파일해야합니다.
무조건 컴파일 시간에 표시되는 UB in main
은이 규칙의 예외 라고 주장 할 수 있습니다 . 또는에서 시작하는 실행 main
이 실제로 보장 된 UB에 도달한다는 것을 컴파일시 증명할 수 있습니다.
나는 아직도 법적 컴파일러 행동이 수류탄이 폭발 생산 등이 있다고 주장 거라고 경우 실행. 또는 더 그럴듯하게 그 정의 main
는 하나의 불법 명령으로 구성됩니다. 나는 당신이 경우 주장 거라고 결코 프로그램을 실행하지, 아직 UB가되지 않았습니다. 컴파일러 자체는 폭발 할 수 없습니다, IMO.
브랜치 내부에 가능하거나 입증 가능한 UB를 포함하는 함수
주어진 실행 경로에있는 UB는 이전의 모든 코드를 "오염"시키기 위해 시간이 거꾸로 도달합니다. 그러나 실제로 컴파일러는 실행 경로가 컴파일시 표시되는 UB로 이어지는 것을 실제로 증명할 수있을 때만 해당 규칙을 활용할 수 있습니다. 예 :
int minefield(int x) {
if (x == 3) {
*(char*)nullptr = x/0;
}
return x * 5;
}
컴파일러는 INT_MIN 및 INT_MAX에서 부호있는 오버플로 UB x
가 x * 5
발생 하는 지점까지 3 개 이외의 모든 항목에서 작동하는 asm을 만들어야 합니다. 이 함수가으로 호출되지 않으면 x==3
프로그램은 물론 UB를 포함하지 않으며 작성된대로 작동해야합니다.
if(x == 3) __builtin_unreachable();
컴파일러에게 x
확실히 3이 아님 을 알리기 위해 GNU C로 작성했을 수도 있습니다 .
실제로는 일반 프로그램의 모든 곳에 "지뢰밭"코드가 있습니다. 예를 들어 정수로 나누면 컴파일러에게 0이 아님을 약속합니다. 모든 포인터 deref는 컴파일러에게 NULL이 아님을 약속합니다.
여기서 "법적"이란 무엇을 의미합니까? 이러한 표준에 따르면 C 표준 또는 C ++ 표준과 모순되지 않는 모든 것은 합법적입니다. 성명서를 실행하고 i = i++;
결과적으로 공룡이 세계를 장악한다면 표준에 위배되지 않습니다. 그러나 그것은 물리학의 법칙과 모순되므로 일어나지 않을 것입니다 :-)
정의되지 않은 동작이 컴파일러와 충돌하더라도 C 또는 C ++ 표준을 위반하지 않습니다. 그러나 이것은 컴파일러의 품질이 향상 될 수 있다는 것을 의미합니다.
이전 버전의 C 표준에는 오류이거나 정의되지 않은 동작에 의존하지 않는 문이있었습니다.
char* p = 1 / 0;
char *에 상수 0을 할당 할 수 있습니다. 0이 아닌 상수를 허용하는 것은 아닙니다. 1 / 0의 값은 정의되지 않은 동작이므로 컴파일러가이 명령문을 수락해야하는지 여부는 정의되지 않은 동작입니다. (오늘날 1/0은 더 이상 "정수 상수 표현"의 정의를 충족하지 않습니다.)