많은 C 입문 교재를 보면 goto를 마치 '절대로 쓰면 안되는 것' 이라던지, '한 번이라도 쓰면 큰일이 나는 것' 과 같이 취급한다.
컴퓨터 과학에 지대한 영향을 끼친 에츠허르 다익스트라(Edsger W. Dijkstra) 옹께서 goto를 절대로 쓰지 말라고 한 것의 영향이
크다고 생각한다.
그렇다면, 왜 쓰지 말아야 할까?
다음 두 코드는 완벽히 동일하게 작동한다. 어느 것이 더 깔끔한가?
int sumTo(int N) {
int res = 0;
for (int i = 1; i <= N; i++) {
res += i;
}
return res;
}
int sumTo(int N) {
int res = 0;
int i = 1;
if (i > N) goto done;
loop:
res += i;
i++;
if (i <= N) goto loop;
done:
return res;
}
당연히 위의 for 문이 더 간결하고 깔끔해 보인다. goto문을 쓴 코드는 마치 어셈블리어 같다.
.sumTo
pushq %rbx
movl $0, %eax
movl $1, %ebx
cmp %rdi, %rbx
jg .DONE
.LOOP
addq %rbx, %rax
addq $1, %rbx
cmp %rdi, %rbx
jle .LOOP
.DONE
popq %rbx
ret
(최대한 단순히 logic만 표현하였다.)
이렇게, 굳이 어셈블리어로 쓰기 위한 것이 아닌 이상, goto 문은 대부분 if와 같은 조건문과 while, for과 같은 반복문으로 대체될 수 있다.
또한, goto를 남용하면 흔히 말하는 스파게티 코드가 나올 수 있다. 만드는 법은 어렵지 않다. 다만, 해석하기 힘들 뿐.
#include <stdio.h>
int main() {
int x = 1, y = 3, z = 4;
goto L2;
L1:
if (++x & 1) goto end;
L3:
z -= 3;
goto L2;
y--;
L2:
x += z++;
if (z & 1) goto L1;
goto L3;
end:
printf("%d, %d, %d\n", x, y, z);
return 0;
}
(이게 바로 예상이 된다면... goto를 마음껏 써도 좋다. 물론 혼자만.)
이렇게 goto를 쓸데없이 남발하면, 보기도 싫고, 해석하기도 힘들고, 건들기는 더욱 두려운 코드가 만들어진다.
그럼, 정말로 goto는 '절대로 쓰면 안되는 것'일까?
의외로, 좋은 사용처가 있다. 바로, 다중 반복문(nested loop)의 탈출이다.
이를 위해 간단한 C-like pseudo code를 작성하면 다음과 같다.
// do something...
for (int i = ...) {
for (int j = ...) {
for (int k = ...) {
// nested for문 전체를 break 해버리고 싶음!
goto out;
}
}
}
out:
// nice break. do something...
오히려 이런 경우에는 goto를 사용하지 않으면 복잡해진다. goto를 안쓰면 flag 변수를 만들어서 매번 검사하고 탈출해야 하는데,
반복문이 깊어지면 깊어질수록, 이는 크게 비효율적일 것이다.
// do something...
int flag = 0;
for (int i = ...) {
for (int j = ...) {
for (int k = ...) {
// nested for문 전체를 break 해버리고 싶음!
flag = 1;
if (flag) break;
}
if (flag) break;
}
if (flag) break;
}
// 이 얼마나 비효율적인가!
물론, flag 변수가 많이 사용되는 것은 비트마스킹을 통해 어느정도 해결할 수 있다. 그럼에도 불구하고, 저 if문은 어쩔 수 없다.
따라서, 다중 반복문에서는 goto를 사용하는 것이 가독성이나 확장성 측면에서 좋다고 할 수 있다.
예외처리에서도 goto는 많이 사용된다.
// in _cpu_down:
if (st->state > CPUHP_TEARDOWN_CPU) {
st->target = max((int)target, CPUHP_TEARDOWN_CPU);
ret = cpuhp_kick_ap_work(cpu);
if (ret)
goto out;
if (st->state > CPUHP_TEARDOWN_CPU)
goto out;
st->target = target;
}
ret = cpuhp_down_callbacks(cpu, st, target);
if (ret && st->state < prev_state) {
if (st->state == CPUHP_TEARDOWN_CPU) {
cpuhp_reset_state(cpu, st, prev_state);
__cpuhp_kick_ap(st);
} else {
WARN(1, "DEAD callback error for CPU%d", cpu);
}
}
out:
cpus_write_unlock();
lockup_detector_cleanup();
arch_smt_update();
cpu_up_down_serialize_trainwrecks(tasks_frozen);
return ret;
이는 linux kernel의 cpu.c 코드에서 에러 처리를 위하여 goto를 사용한 모습이다.
오류를 발생하는 ret 변수는 _cpu_down 함수 내에서 할당되고, 처리된다.
따라서, 어떠한 함수 내에서 예외를 처리할 수 있다면, goto문을 사용하는 것이 확실하고, 깔끔한 방법 중 하나라는 것을 확인할 수 있다.
아니다. 이것이 goto의 한계라고 할 수 있다. goto는 일반적으로 한 함수 안에서만 점프할 수 있다. 즉, 다음과 같은 코드는 컴파일 오류를 발생시킨다.
void foo() {
bar();
L1: // useless
return;
}
void bar() {
goto L1;
// 이 함수에는 L1이 없는데?
}
int main() {
foo();
return 0;
}
이렇게 여러 함수를 옮겨다니기 위해서는 non-local jump인 setjmp와 longjmp를 사용해야만 한다.
즉, goto로 예외를 처리하기 위해서는 꼭 그 함수 내에서 (다른 errorful한 함수를 호출하지 않고) 처리할 수 있을 정도로 단순한 예외여야 한다.
(물론 nonlocal jump를 쓰더라도 메모리 해제 등등 생각해야 할 것이 많다.)
goto는 마냥 나쁜것은 아니다. 대부분의 상황에서는 코드를 지저분하게 만들지만, 특정한 상황에서는 오히려 코드를 보기 좋게 만들 수 있다.
따라서, 적재적소에 쓰면서 코드를 짤 필요가 있다.
- 웬만해선
goto는if나for로 대체될 수 있다. - 다중 반복문이나 간단한 예외처리에서는
goto가 더 좋을수도? - 근데 non-local jump는 안됨.