c 언어 관계연산과 논리 연산 순서 관한 질문입니다.
글쓴이: tph02361 / 작성시간: 월, 2017/03/20 - 5:09오후
#include
void main()
{
int nR, w=0, x=-1, y=-1, z=1;
nR=w++ || x++ &&++y || ++z;
printf("%d %d %d %d %d\n", w, x, y, z, nR);
}
여기서 각각의 변수가 최종적으로 어떻게 나오는지는 알겠는데 순서가 헷갈려서 질문합니다.
w++ || x++ 같은 경우는 앞에 w가 0이라 ||뒤에 x++~이 연산되는 건 알겠습니다. 그럼 w변수에 +1 연산을 하고 x를 검토하는 건가요 아니면 x도 검토하고 그다음에 w랑 x에 +1 연산을 하게 되는 건가요?
Forums:
C언어 스팩상 둘 다 가능합니다.
C언어 스팩상 둘 다 가능합니다.
경험해 본 것 중에,
GCC는 전자로 동작했고,
Visual Studio 6.0 C 컴파일러는 후자로 동작했습니다.
질문과 같은 내용은 한번 고민해보고 다시는 그렇게
질문과 같은 내용은 한번 고민해보고 다시는 그렇게 작성하시지 않는것이 좋습니다.
안전한 프로그램을 작성하세요!
http://story.wisedog.net/misra-c-%EC%95%88%EC%A0%84%ED%95%9C-c-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%98-%EC%BD%94%EB%94%A9-%ED%91%9C%EC%A4%80/
...
음 링크하신 글을 읽어보니 좀 이상한 구석이 있는데,
> 논리적 AND 연산(&&)이나 논리적 OR연산(||) 의 우항 피연산자는 Side Effect를 가져서는 안된다는 내용이지요. 이 경우 컴파일러마다 실행 순서가 달라져서 개발자가 의도하지 않은 결과가 나올 수 있습니다.
그렇지 않습니다. &&와 || 연산의 경우 무조건 왼쪽을 먼저 계산하고 &&의 경우 왼쪽 값이 참일 때만 오른쪽을 계산해야 한다고 C 언어 표준에서 규정하고 있습니다. (||의 경우에는 왼쪽이 거짓일 때만 오른쪽을 계산.)
물론 한 줄에서 ++ 연산자를 여러 개 쓰면 읽는 사람이 헷갈리니까 이렇게 쓰는 게 별로 좋은 스타일은 아니지만, 표준에 따르면 아무 문제가 없는 방식입니다. 그리고 side effect를 불러일으키는 함수를 &&로 연결하는 건 대단히 흔합니다. 예를 들자면,
불로그의 내용에 대한 정당한 지적이십니다만,
불로그의 내용에 대한 정당한 지적이십니다만,
MISRA 코딩룰에서 하지 말라고 하는 사실은 변함없습니다.
작성한 프로그램을 납품 못하게 될수도 있습니다.
복잡하군요.
복잡하군요.
1. 뭐 일단, 딱히 위험한 프로그램은 아닙니다.
복잡한 full expression 안의 증감 연산자가 때로 문제를 발생시키곤 하는 건 사실입니다만,
(멀리 볼 것도 없이 최근에 https://kldp.org/node/157199 가 올라왔었지요.)
문제는 증감 연산자 자체가 아니라 sequence point 및 side effect에 대한 규칙 때문입니다.
C99 기준으로, 인접한 두 sequence point 사이에서 객체 하나는 (1) 그 값이 최대 한 번만 변경될 수 있으며
(2) 그 때 그 객체의 이전 값은 새로 저장될 값을 계산하기 위한 목적으로만 읽힐 수 있습니다.
https://kldp.org/node/157199에서 예를 들어 설명드리면,
result = a++ + a;
는 (2)를 위반한 것이고,a++ + a++
는 (1)을 위반한 것이지요.C11에서는 전통적인 sequence point 개념에 더불어 sequenced before/after, indeterminately sequenced, unsequenced 개념이 추가되어 조금 더 설명하기 복잡해졌습니다. 그래도 조금 언급하자면 C99에서 undefined behavior로 보내버렸던 몇몇 코드가 C11에서는 유효해진 경우가 좀 있어요. 대표적으로
a = a = 0;
. 이 코드는 C99 표준을 따지면 undefined behavior를 유발한다는데, 실제로 이 코드에 대해 제 예상을 벗어나는 동작을 일으키는 구현체가 있다면 꼭 보고 싶습니다. :)아무튼,
(1) 주어진 full expression
nR=w++ || x++ &&++y || ++z;
에서 증감 연산자가 사용되는 대상인 w, x, y, z 모두 한 번씩만 변경되고, 이전 값이 다음 값을 결정하기 위해서만 쓰이기 때문에 C99 기준으로 완전히 합법적입니다.(2) 게다가 논리 연산자 ||와 &&는 단축 평가를 하면서, 왼쪽 피연산자를 평가한 후 오른쪽 피연산자를 평가할 때는 반드시 그 사이에 sequence point를 찍어줍니다. 그래서 사실
nR=x++ || x++ &&++x || ++x;
같이 써도 아무 문제없습니다.2. 그 사실과 별개로, 변수 w가 증가된 뒤 표현식 x++이 평가되는 것인지, 아니면 w의 증가가 잠시 미루어질 수 있는지에 대해서는 정해지지 않았습니다.
표준에 따르면 표현식 평가에서 발생하는 side effect는 반드시 다음 sequence point 이전에 모두 일어나야 합니다. 객체를 변경하는 것은 대표적인 side effect이므로, 표준에 의거 표현식 x++을 평가하기 전에 w를 증가시켜야 합니다.
그러나 이러한 표준의 명세는 실제 구현체의 동작을 규정하는 것이 아니라, 일종의 abstract machine의 동작을 정의한 것입니다. 구현체는 이 abstract machine을 그대로 따라서 동작할 의무가 없습니다. 여기에 대해서 observable behavior라는 개념이 도입되는데, 쉽게 풀어 설명하자면 "실제 구현체가 겉보기에 abstract machine처럼 동작하는 것으로 보인다면 그 정도로 충분하다" 정도가 되겠습니다.
질문자님의 예제에서 변수 w의 증가와 표현식 x++의 평가 순서를 뒤바꾸어도 그 차이를 아무도 볼 수 없습니다. abstract machine 기준으로는 전자가 후자보다 반드시 앞서야 하지만 실제 구현에서는 어느 쪽이 앞서야 하는지 정해지지는 않는다는 말씀입니다. 실제로 이를 근거로 많은 컴파일러가 소위 instruction scheduling을 통해 프로그램의 겉보기 동작을 바꾸지 않는 범위에서 명령어의 순서를 뒤바꾸는 최적화를 합니다.
결과적으로 printf 함수가 변수 w의 증가된 값을 찍기만 한다면 그 과정은 아무래도 상관 없는 것입니다.
Disclaimer. 모종의 이유로 프로그램 중간에 실행 흐름이 끊긴 채 누군가가 실행 컨텍스트를 들여다본다면야 당연히 차이가 생기죠. 가장 대표적인 경우가 바로 디버거로 중단점을 찍는 경우인데, 표준에서 그런 경우를 위해 뭐 따로 보장해주는 건 없습니다. 그런 건 컴파일러가 지원해주기 마련이니 컴파일러에게 디버그 모드로 컴파일해달라고 하세요.
번외. 사실 논리 연산자는 side effect가 있는 표현식을 조건부로 실행시키는 데 꽤 편리합니다. 안전하기도 하고요.
jick님이 올려주신 코드처럼 if문 하나를 덜 쓰고 코딩할 수 있게 만들어 주지요. 대부분의 경우 컴파일된 바이나리를 뜯어보면 if문 쓴 것과 별 차이 없는 결과물이 만들어지기는 합니다만.
사람에 따라서 그런 코드가 비직관적이고 위험하다고 생각하시는 분들도 있긴 한데 사실 전 어느 정도는 취향 차이라고 봐요. 예컨대 저는 perl의 unless 구문이 영 헷갈립니다. 특히 else문 안에서 쓰이거나 postfix control로 쓰이면 더더욱. 근데 돌아다니는 perl code를 보면 저와 다르게 생각하시는 분들도 많은 것 같더라고요.
다만 물론 여기에는 정도라는 게 있는 법이죠. ioccc 같은 데 나가지 않는 한 &&나 ||로 끝없이 이어지는 코드를 누가 좋아하겠습니까. 혹시 레퍼런스 안 찾아보고 질문자님의 코드가
nR=((w++||(x++&&++y))||++z);
순서로 묶인다는 걸 바로 읽어내신 분?그런 의미에서 볼 때 MISRA-C의 해당 규정도 나름 납득할 만 하다고 생각합니다. undefined behavior만 피한다고 능사가 아니니까요. 프로그래밍하는 사람 입장에서는 실수를 줄이고, 읽는 사람 입장에서 오해를 줄일 수 있다면 괜찮지 않겠습니까.
댓글 달기