f (i = -1, i = -1) 동작이 정의되지 않은 이유는 무엇입니까? 그 반대인지 명확하지 않습니다 . 두

나는 평가 위반 순서에 대해 읽고 있었고 , 그들은 나를 당황스럽게하는 예를 제시합니다.

1) 스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용에 비해 순서가 맞지 않으면 동작이 정의되지 않습니다.

// snip
f(i = -1, i = -1); // undefined behavior

이러한 맥락에서, iA는 스칼라 객체 명백하게 수단

산술 유형 (3.9.1), 열거 유형, 포인터 유형, 멤버 유형에 대한 포인터 (3.9.2), std :: nullptr_t 및 이러한 유형의 cv 규정 버전 (3.9.3)을 통칭하여 스칼라 유형이라고합니다.

이 경우 진술이 어떻게 모호한 지 알 수 없습니다. 첫 번째 또는 두 번째 인수가 먼저 평가되는지 여부에 관계없이로 i끝나고 -1두 인수도 모두 같습니다 -1.

누군가가 명확히 할 수 있습니까?


최신 정보

나는 모든 토론에 정말 감사합니다. 지금까지 나는 @harmic의 대답을 좋아 합니다. 왜냐하면이 문장을 정의하는 데 따르는 함정과 복잡성이 언뜻보기에 눈에 띄게 보이지만 복잡합니다. @ acheong87 은 참조를 사용할 때 발생하는 몇 가지 문제를 지적하지만이 질문의 순서가없는 부작용 측면과 직각이라고 생각합니다.


요약

이 질문에 많은 관심을 기울 였으므로 주요 요점 / 답변을 요약하겠습니다. 먼저, “왜”가 “무슨 원인 “, “무슨 이유 “, “무엇을 목적으로 ” 와 밀접한 관련이 있지만 미묘하게 다른 의미를 가질 수 있다는 점에 대해 약간의 왜곡이 있습니다 . 나는 그들이 왜 “왜”라는 의미로 대답을 그룹화 할 것입니다.

어떤 원인으로

주요 대답은 여기에서 유래 폴 드레이퍼 와, 마틴 J는 광범위한 답변으로 유사하지만 기여. 폴 드레이퍼의 대답은

동작이 무엇인지 정의되어 있지 않기 때문에 정의되지 않은 동작입니다.

그 대답은 C ++ 표준이 말하는 것을 설명 할 때 전반적으로 매우 좋습니다. 또한 같은 UB 일부 관련 사건을 해결 f(++i, ++i);하고 f(i=1, i=-1);. 첫 번째 관련 사례에서 첫 번째 인수가 i+1두 번째 i+2인지 아니면 두 번째 또는 그 반대인지 명확하지 않습니다 . 두 번째로, i함수 호출 후 1 또는 -1이어야하는지 명확하지 않습니다 . 이 두 경우 모두 다음 규칙에 속하기 때문에 UB입니다.

스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용에 비해 순서가 맞지 않으면 동작이 정의되지 않습니다.

따라서 f(i=-1, i=-1)프로그래머의 의도가 (IMHO) 명백하고 모호하지 않더라도 동일한 규칙에 속하기 때문에 UB이기도합니다.

폴 드레이퍼는 또한 그의 결론에서

동작을 정의했을 수 있습니까? 예. 정의 되었습니까? 아니.

“무슨 이유 / 목적이 f(i=-1, i=-1)정의되지 않은 행동 으로 남게 되었습니까?”

어떤 이유로 / 목적으로

C ++ 표준에는 약간의 감독 (심지어 부주의 할 수 있음)이 있지만, 많은 누락이 적절하고 합리적인 목적으로 사용됩니다. 목적이 종종 “컴파일러 작성기의 작업을 더 쉽게”만들거나 “더 빠른 코드”라는 것을 알고 있지만 , UB로 떠날만한 충분한 이유가 있는지에 대해 주로 관심이있었습니다 f(i=-1, i=-1) .

harmicsupercat 은 UB 의 이유 를 제공하는 주요 답변을 제공합니다 . Harmic은 표면적으로 원자 할당 작업을 여러 기계 명령어로 분리 할 수있는 최적화 컴파일러가 최적의 속도를 위해 해당 명령어를 추가로 인터리브 할 수 있다고 지적합니다. 이것은 매우 놀라운 결과로 이어질 수 i있습니다. 그의 시나리오에서 -2로 끝납니다! 따라서 고조파 는 연산에 순서가 지정되지 않은 경우 변수에 동일한 값 을 두 번 이상 할당하면 어떻게 악영향을 미칠 수 있는지 보여줍니다 .

supercat은 f(i=-1, i=-1)해야 할 일 을하려고하는 함정에 대한 관련 설명을 제공합니다 . 그는 일부 아키텍처에서는 동일한 메모리 주소에 대한 여러 동시 쓰기에 대한 엄격한 제한이 있다고 지적합니다. 우리가보다 사소한 것을 다루는 경우 컴파일러는 이것을 잡기가 어려울 수 있습니다 f(i=-1, i=-1).

davidf 는 또한 하모닉 과 매우 유사한 인터리빙 지침의 예를 제공합니다.

각 고조파, supercat 및 davidf의 예는 다소 고안되었지만, 함께 모아서 f(i=-1, i=-1)정의되지 않은 행동을해야하는 확실한 이유를 제공합니다 .

Paul Draper의 답변이 “원인”부분을 더 잘 다루었음에도 불구하고, 왜 모든 의미를 다루는 데 최선을 다했기 때문에 harmic의 답변을 받아 들였습니다.

다른 답변들

JohnB는 (일반 스칼라 대신) 오버로드 된 할당 연산자를 고려하면 문제가 발생할 수 있다고 지적합니다.



답변

연산은 순서가 지정되지 않았으므로 할당을 수행하는 명령을 인터리브 할 수 없다는 것은 말할 것도 없습니다. CPU 아키텍처에 따라 그렇게하는 것이 가장 좋습니다. 참조 된 페이지는 다음을 나타냅니다.

A가 B보다 먼저 시퀀싱되지 않고 B가 A보다 먼저 시퀀싱되지 않으면 두 가지 가능성이 있습니다.

  • A와 B의 평가는 순서가 없습니다 : 그것들은 어떤 순서로든 수행 될 수 있으며 겹칠 수 있습니다 (단일 실행 스레드 내에서 컴파일러는 A와 B를 포함하는 CPU 명령어를 인터리브 할 수 있습니다)

  • A와 B의 평가는 불확실한 순서로 수행된다 : 어떤 순서로도 수행 될 수 있지만 겹치지 않을 수있다 : A는 B보다 먼저 완료되거나 B는 A보다 먼저 완료된다 평가됩니다.

수행되는 작업이 값 -1을 메모리 위치에 저장하는 것으로 가정하면 그 자체로는 문제를 일으키는 것처럼 보이지 않습니다. 그러나 컴파일러는 동일한 효과를 갖는 별도의 명령 세트로 컴파일러를 최적화 할 수 없지만 동일한 메모리 위치에서 다른 작업과 작업이 인터리브되면 실패 할 수 있습니다.

예를 들어, -1 in 값을로드하는 것과 비교하여 메모리를 0으로 설정 한 다음 감소시키는 것이 더 효율적이라고 가정하십시오.

f(i=-1, i=-1)

될 수 있습니다 :

clear i
clear i
decr i
decr i

이제 저는 -2입니다.

아마도 가짜 예일 수 있지만 가능합니다.


답변

먼저, “스칼라 객체”는 같은 유형을 의미 int, float또는 포인터 (참조 ++ C에서 스칼라 객체 무엇입니까? ).


둘째, 더 분명해 보일 수 있습니다.

f(++i, ++i);

정의되지 않은 동작이 있습니다. 그러나

f(i = -1, i = -1);

덜 명확합니다.

약간 다른 예 :

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

어떤 과제가 “마지막” i = 1또는 i = -1? 표준에 정의되어 있지 않습니다. 실제로는 그 방법 i이 될 수 있습니다 5(이 사건이 어떻게 일어날 수 있는지에 대한 완전히 그럴듯한 설명은 고조파의 대답을 참조하십시오). 또는 프로그램이 segfault 일 수 있습니다. 또는 하드 드라이브를 다시 포맷하십시오.

그러나 지금 당신은 묻습니다. “나의 예는 어떻습니까? -1두 과제에 같은 값 ( )을 사용했습니다 . 그 점에 대해 분명하지 않은 것은 무엇입니까?”

C ++ 표준위원회가 설명한 방식을 제외하고는 정확합니다.

스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용에 비해 순서가 맞지 않으면 동작이 정의되지 않습니다.

그들은 당신의 특별한 경우에 특별한 예외를 만들 있었지만 그렇지 않았습니다. (그리고 왜해야합니까? 어쩌면 어떤 용도로 사용됩니까?) i그래도 여전히 그렇습니다 5. 또는 하드 드라이브가 비어있을 수 있습니다. 따라서 귀하의 질문에 대한 답변은 다음과 같습니다.

동작이 무엇인지 정의되어 있지 않기 때문에 정의되지 않은 동작입니다.

(많은 프로그래머들이 “정의되지 않은”은 “무작위”또는 “예측할 수없는”을 의미한다고 생각하기 때문에 강조 할 가치가있다. 표준에 의해 정의되지 않은 것을 의미한다. 행동은 100 % 일관되고 여전히 정의되지 않을 수있다.)

동작을 정의했을 수 있습니까? 예. 정의 되었습니까? 따라서 “정의되지 않음”입니다.

말했다 즉, 컴파일러는 하드 드라이브를 포맷 것은 아닙니다 … 그것은 것을 의미한다 “정의되지 않은” 그것은 여전히 표준을 준수하는 컴파일러 될 것이다. 실제로 g ++, Clang 및 MSVC가 모두 예상대로 작동합니다. 그들은 단지 “하지 않았다”.


다른 질문은 C ++ 표준위원회왜이 부작용을 시퀀싱하지 않기로 선택했을까요? . 이 답변에는위원회의 역사와 의견이 포함됩니다. 또는 C ++ 에서이 부작용을 시퀀싱하지 않는 것이 좋은 점은 무엇입니까? 표준위원회의 실제 추론인지 여부에 상관없이 모든 정당성을 허용합니다. 여기 또는 programmers.stackexchange.com에서 이러한 질문을 할 수 있습니다.


답변

두 값이 동일하기 때문에 규칙에서 예외를 만들지 않는 실질적인 이유 :

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

이것이 허용 된 경우를 고려하십시오.

몇 달 후, 변화해야 할 필요성이 생겼습니다.

 #define VALUEB 2

겉보기에 무해하지 않습니까? 그러나 갑자기 prog.cpp는 더 이상 컴파일되지 않습니다. 그러나 컴파일은 리터럴의 가치에 의존해서는 안된다고 생각합니다.

결론 : 규칙의 예외는 상수의 값 (유형이 아닌)에 따라 성공적으로 컴파일되므로 예외는 없습니다.

편집하다

@HeartWare는A DIV B 일부 언어에서는 B0 일 때 양식의 상수 표현식이 허용되지 않으며 컴파일이 실패 한다고 지적했습니다 . 따라서 상수를 변경하면 다른 곳에서 컴파일 오류가 발생할 수 있습니다. IMHO는 불행합니다. 그러나 그러한 것들을 피할 수없는 것으로 제한하는 것이 좋습니다.


답변

혼란은 상수 값을 지역 변수에 저장하는 것이 C가 실행되도록 설계된 모든 아키텍처에서 하나의 원자 명령이 아니라는 것입니다. 이 경우 코드가 실행되는 프로세서는 컴파일러보다 중요합니다. 예를 들어, 각 명령어가 완전한 32 비트 상수를 전달할 수없는 ARM에서 int를 변수에 저장하려면 하나 이상의 명령어가 필요합니다. 한 번에 8 비트 만 저장할 수 있고 32 비트 레지스터에서 작동해야하는이 의사 코드의 예는 int32입니다.

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

컴파일러가 최적화를 원한다면 동일한 시퀀스를 두 번 인터리브 할 수 있으며 i에 어떤 값이 쓰여질 지 모른다는 것을 상상할 수 있습니다. 그가 똑똑하지 않다고 가정 해 봅시다.

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

그러나 내 테스트에서 gcc는 동일한 값이 두 번 사용되어 한 번 생성되고 이상한 일을하지 않는다는 것을 인식하기에 친절합니다. 나는 -1, -1을 얻지 만 상수도조차도 분명하지 않은 것으로 간주하는 것이 중요하므로 내 예제는 여전히 유효합니다.


답변

“유용한”컴파일러가 완전히 예상치 못한 동작을 유발할 수있는 무언가를 수행 할 수있는 몇 가지 이유가있을 경우 동작은 일반적으로 정의되지 않은 것으로 지정됩니다.

변수가 여러 번 기록되어 별개의 시간에 기록이 이루어 지도록 보장하지 않는 경우, 일부 종류의 하드웨어는 이중 포트 메모리를 사용하여 여러 “저장”작업을 다른 주소로 동시에 수행 할 수 있습니다. 그러나 일부 듀얼 포트 메모리는 기록 된 값이 일치하는지 여부에 관계없이 두 저장소가 동일한 주소를 동시에 누르는 시나리오를 명시 적으로 금지합니다.. 그러한 머신의 컴파일러가 동일한 변수를 작성하는 순서없는 두 번의 시도를 발견하면, 컴파일을 거부하거나 두 쓰기가 동시에 스케줄 될 수 없도록 할 수 있습니다. 그러나 액세스 중 하나 또는 둘 다가 포인터 또는 참조를 통해 수행되는 경우 컴파일러가 두 쓰기가 동일한 저장 위치에 도달 할 수 있는지 여부를 항상 알 수는 없습니다. 이 경우 쓰기가 동시에 예약되어 액세스 시도에 하드웨어 트랩이 발생할 수 있습니다.

물론 누군가가 그러한 플랫폼에서 C 컴파일러를 구현할 수 있다는 사실은 원자 적으로 처리하기에 충분히 작은 유형의 저장소를 사용할 때 이러한 동작이 하드웨어 플랫폼에서 정의되어서는 안된다고 제안하지 않습니다. 컴파일러가 알지 못하는 경우 서로 다른 두 가지 값을 순서대로 저장하지 않으면 이상이 발생할 수 있습니다. 예를 들면 다음과 같습니다.

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

컴파일러가 “moo”에 대한 호출을 인라인하고 “v”를 수정하지 않는다고 말할 수 있으면 5 대 v를 저장 한 다음 6 대 * p를 저장 한 다음 5를 “zoo”로 전달한 다음 v의 내용을 “zoo”로 전달하십시오. “zoo”가 “v”를 수정하지 않으면 두 호출에 다른 값을 전달할 방법이 없어야하지만 어쨌든 쉽게 발생할 수 있습니다. 반면에 두 상점 모두 같은 값을 쓸 경우 이러한 기이함이 발생할 수 없으며 대부분의 플랫폼에서 구현이 기묘한 일을하는 합리적인 이유가 없을 것입니다. 불행히도, 일부 컴파일러 작성자는 “표준이 허용하기 때문에”이외의 어리석은 행동에 대한 변명이 필요하지 않으므로 이러한 경우조차 안전하지 않습니다.


답변

경우 대부분의 구현에서 결과가 동일하다는 사실 은 부수적입니다. 평가 순서는 아직 정의되지 않았습니다. 고려 f(i = -1, i = -2): 여기, 순서가 중요합니다. 귀하의 예에서 중요하지 않은 유일한 이유는 두 값이 모두 사고가 발생한 것입니다 -1.

식이 정의되지 않은 동작이있는 식으로 지정된 경우, 악의적으로 호환되는 컴파일러 f(i = -1, i = -1)는 실행 을 평가 하고 중단 할 때 부적절한 이미지를 표시 할 수 있지만 여전히 완전히 올바른 것으로 간주됩니다. 운 좋게도 내가 아는 컴파일러는 없습니다.


답변

함수 인수 표현식의 시퀀싱과 관련된 유일한 규칙은 다음과 같습니다.

3) 함수를 호출 할 때 (함수가 인라인인지 여부와 명시적인 함수 호출 구문 사용 여부) 인수 표현식 또는 호출 된 함수를 지정하는 접미사 표현식과 관련된 모든 값 계산 및 부작용은 다음과 같습니다. 호출 된 함수의 본문에서 모든 표현식 또는 명령문을 실행하기 전에 순서화됩니다.

이것은 인수 표현식 사이의 시퀀싱을 정의하지 않으므로이 경우에 끝납니다.

1) 스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용에 비해 순서가 맞지 않으면 동작이 정의되지 않습니다.

실제로 대부분의 컴파일러에서 인용 한 예제는 “하드 디스크 지우기”및 기타 이론적으로 정의되지 않은 동작 결과와는 달리 제대로 실행됩니다.
그러나 할당 된 두 값이 동일하더라도 특정 컴파일러 동작에 따라 달라 지므로 책임이 있습니다. 또한 다른 값을 할당하려고하면 결과가 “정확하게”정의되지 않은 것입니다.

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}