goto 문
goto의 함수 내부 레이블 점프, 초기화 우회 제한, 스코프 이탈 시 객체 파괴와 구조적인 대안을 정확히 구분합니다.
goto는 프로그램의 아무 위치로나 이동하는 명령이 아닙니다. 현재 함수 안에 있는 식별자 레이블로 제어를 무조건 옮기는 점프 문입니다.
문법은 짧지만 출발점과 도착점 사이의 초기화, 상태 갱신, 수동 정리 코드를 건너뛸 수 있습니다. 새 코드에서는 더 좁은 제어 범위를 표현하는 break, continue, return, helper 함수와 RAII를 먼저 사용합니다.
C++ · GOTO SCOPE CONTRACT
goto는 현재 함수 안의 레이블로만 이동합니다. 합법적인 도착점인지와 점프 중 활성 상태가 달라지는 automatic 객체를 함께 확인해야 합니다.
| 감사 항목 | 언어 계약 | 코드에서 확인할 결과 |
|---|---|---|
| 도착 레이블 | 식별자 레이블은 현재 함수 안에 있어야 하며 같은 함수에서 이름이 중복될 수 없음 | 다른 함수나 전역 목적지로 이동할 수 없음 |
| 스코프 진입 | 도착점에서 새로 활성화될 automatic 변수의 초기화가 모두 vacuous하지 않다면 jump로 진입할 수 없음 | std::string 같은 비자명한 초기화를 건너뛰는 코드는 ill-formed |
| 스코프 이탈 | 이미 생성됐지만 도착점에서는 활성 상태가 아닌 automatic 객체를 생성의 역순으로 파괴 | goto도 정상적인 소멸자 호출을 억제하지 않음 |
| raw pointer 소유권 | pointer 변수의 파괴는 가리키는 동적 배열에 delete[]를 수행하지 않음 |
수동 해제 경로가 없으면 누수; RAII owner로 교체 |
- 도착점 · 현재 함수의 레이블
- 레이블은 함수 안에서 고유해야 합니다. 다른 함수나 전역 위치로 점프할 수 없습니다.
- 진입 · 초기화 우회 제한
- 도착점에서 새로 활성화될 변수의 초기화가 모두 vacuous한 경우가 아니라면 그 선언을 건너뛰어 들어갈 수 없습니다.
- 이탈 · automatic 객체 파괴
- 스코프를 나가며 이미 생성된 객체를 역순으로 파괴합니다.
goto도 소멸자를 억제하지 않습니다. - 소유권 · raw pointer는 해제하지 않음
- pointer 변수의 수명 종료와 동적 배열의
delete[]는 별개입니다. RAII owner가 누수 경로를 없앱니다.
금지되는 진입
goto ready;
std::string text = "ready";
ready: use(text); // ill-formed
도착점에서 필요한 text 초기화를 점프가 우회합니다.
허용되는 이탈
{
File file{"log"};
if (failed) goto done;
}
done: report();
점프 전에 생성된 file은 스코프를 나갈 때 파괴된 뒤 레이블에 도착합니다.
점프의 합법성과 자원 안전성은 별도 질문입니다. 합법적인 goto도 수동 delete[]나 상태 커밋을 건너뛸 수 있으므로, 소유권은 RAII 객체로 표현하고 부수 효과를 경로별로 감사합니다.
goto와 레이블의 기본 규칙
void finish();
void process() {
goto done;
// 건너뛸 수 있는 문장
done:
finish();
}이하 코드 조각은 work, finish, File, Lock, use, report, report_error, Grid 같은 애플리케이션 타입과 helper가 이미 선언되어 있다고 가정하고 제어 이동 계약만 보여줍니다.
식별자 레이블은 같은 함수 안에서 중복될 수 없고, 선언보다 앞에서 참조할 수도 있습니다. 그러나 goto의 대상 레이블은 반드시 현재 함수 안에 있어야 합니다. 다른 함수의 레이블로 점프하거나 레이블을 함수 밖의 공용 목적지처럼 사용할 수 없습니다.
void process(bool skip) {
if (skip) {
goto done;
}
work();
done:
finish();
}이 코드는 문법적으로 단순하지만 work()를 건너뛰는 경로를 레이블까지 추적해야 합니다. 같은 의도라면 조건문이나 조기 return이 보통 더 읽기 쉽습니다.
초기화를 건너뛰어 스코프 안으로 들어갈 수 없다
함수 안의 레이블이라고 해서 모든 점프가 허용되는 것은 아닙니다. 점프의 도착점에서 활성 상태가 되지만 출발점에서는 아직 활성 상태가 아닌 automatic 변수가 있다면, 그 변수들의 초기화가 모두 vacuous한 경우가 아닌 한 점프로 들어갈 수 없습니다.
다음 코드는 message의 비자명한 초기화를 건너뛰어 그 스코프 안으로 들어가므로 ill-formed입니다.
#include <string>
void print_ready() {
goto ready; // 오류: message 초기화를 우회
std::string message = "ready";
ready:
(void)message;
}따라서 “forward goto는 항상 금지”도, “같은 함수면 어디든 이동 가능”도 정확하지 않습니다. 출발점과 도착점에서 활성인 block variable 집합을 비교하고, 새로 활성화될 변수의 초기화를 점프가 우회하는지 확인해야 합니다.
스코프를 빠져나가면 생성된 automatic 객체는 파괴된다
goto가 스코프를 빠져나간다고 해서 이미 생성된 automatic 객체의 소멸자가 생략되지는 않습니다. 제어가 스코프를 나갈 때 그 스코프에서 생성된 automatic 객체는 생성의 역순으로 파괴됩니다. return, break, continue, goto처럼 어떤 제어 이동으로 빠져나갔는지와 무관합니다.
void write_log(bool failed) {
{
File file{"result.log"};
Lock lock{file};
if (failed) {
goto done;
}
file.write("ok");
} // 정상 경로에서도 lock, file 순서로 파괴
done: // 점프 경로에서도 lock, file 순서로 파괴된 뒤 도착
report();
}이 규칙이 RAII의 기반입니다. 파일, 잠금, 메모리 같은 소유권을 객체 수명에 묶으면 구조적인 조기 종료에서도 정리가 유지됩니다.
raw pointer 누수는 소멸자 생략이 아니라 수동 소유권 문제다
raw pointer 변수의 수명이 끝나도 그 포인터가 가리키는 동적 배열은 자동으로 delete[]되지 않습니다. 다음 누수는 goto가 포인터의 소멸을 막아서가 아니라, 점프가 수동 delete[] 호출을 건너뛰고 raw pointer 자체가 소유권을 정리하지 않기 때문에 발생합니다.
void process_data(bool failed) {
int* data = new int[100];
if (failed) {
goto done;
}
use(data);
delete[] data;
return;
done:
report_error(); // delete[] data가 없어 동적 배열 누수
}소유권을 표준 RAII 타입으로 표현하면 별도의 정리 레이블이 필요하지 않습니다.
#include <memory>
void process_data(bool failed) {
auto data = std::make_unique<int[]>(100);
if (failed) {
report_error();
return;
}
use(data.get());
} // 모든 return 경로에서 unique_ptr이 동적 배열을 해제더 좁은 구조로 의도를 표현한다
이 장에서 다루는 일반 제어문을 기준으로 break는 가장 안쪽 반복문 또는 switch를 끝내고, continue는 가장 안쪽 반복문의 반복 계속 지점으로 이동합니다. 둘 다 여러 중첩 반복을 한 번에 끝내지는 않습니다.
여러 반복을 포함한 탐색 자체가 하나의 작업이라면 helper 함수로 분리하고 return으로 그 작업을 끝내는 편이 명확합니다.
bool contains_target(const Grid& grid, int target) {
for (const auto& row : grid) {
for (int value : row) {
if (value == target) {
return true;
}
}
}
return false;
}C++ · NARROWEST CONTROL BOUNDARY
점프 목적을 가장 좁게 표현하는 문법을 고릅니다. 반복 제어, 함수 종료, 자원 정리는 서로 다른 책임이며 하나의 정리 레이블로 합칠 이유가 없습니다.
| 의도 | 우선 구조 | 실제 경계와 점검 |
|---|---|---|
| 가장 가까운 대상 문장 종료 | break |
이 장의 일반 제어문에서는 가장 안쪽 반복문 또는 switch만 끝내고 그 다음 문장으로 이동 |
| 현재 반복의 나머지 생략 | continue |
가장 안쪽 반복문의 반복 계속 지점으로 이동; switch는 대상이 아님 |
| 중첩 탐색 작업 전체 종료 | helper 함수 + return |
탐색의 성공·실패를 함수 결과로 표현하고 모든 나가는 스코프의 automatic 객체를 파괴 |
| 작은 기존 구조에 종료 신호 전달 | 상태 값 + 바깥 조건 | 수정 범위는 작지만 신호 검사가 여러 단계에 퍼지는지 확인 |
| 메모리·파일·잠금 정리 | RAII owner | unique_ptr, container, handle 객체의 수명으로 정리; raw cleanup label에 의존하지 않음 |
남아 있는 레거시 goto |
출발점·레이블·경로 감사 | 현재 함수 대상, 초기화 우회, 역순 파괴, 건너뛴 수동 부수 효과를 모두 확인 |
break· 대상 문장 하나 종료- 이 장의 일반 제어문에서는 가장 안쪽 반복문 또는
switch만 끝냅니다. continue· 현재 반복 생략- 가장 안쪽 반복문의 반복 계속 지점으로 갑니다.
switch는 대상이 아닙니다. - helper +
return· 작업 전체 종료 - 중첩 탐색을 함수 책임으로 묶고 결과와 종료 이유를 함께 표현합니다.
- 상태 값 · 작은 기존 구조 보완
- 바깥 반복마다 신호 검사가 퍼져 오히려 흐름을 숨기지 않는지 확인합니다.
- RAII · 자원 수명 정리
- 메모리, 파일, 잠금은 owner 객체가 모든 구조적 종료 경로에서 정리합니다.
- 레거시
goto· 네 경계 감사 - 현재 함수 레이블, 초기화 우회, automatic 객체 파괴, 수동 부수 효과 누락을 확인합니다.
수동 소유권 경로
new[] → 여러 점프 경로 → 경로마다 delete[] 필요
한 경로라도 해제가 없으면 누수입니다. pointer 변수의 소멸은 동적 배열을 해제하지 않습니다.
RAII 소유권 경로
make_unique<T[]> → 조기 return → owner 소멸
제어 경로와 정리 경로를 분리하지 않고 객체 수명 하나로 소유권을 닫습니다.
새 코드에서 goto는 “여러 단계를 한 번에 빠져나가는 편의”보다 제어 범위를 숨기는 비용이 큽니다. 기존 코드에 남겨야 한다면 합법성뿐 아니라 건너뛴 상태 변경과 수동 정리까지 경로별로 검증합니다.
레거시 goto 감사 순서
출발 문장과 도착 레이블이 같은 함수 안에 있는지 확인합니다.
점프의 도착점에서 새로 활성화될 automatic 변수가 있다면 그 초기화가 모두 vacuous한지 확인합니다.
빠져나가는 각 스코프에서 이미 생성된 automatic 객체가 역순으로 파괴된다는 점을 반영합니다.
점프 사이에 수동 delete[], close(), 상태 커밋처럼 객체 수명에 묶이지 않은 작업이 있는지 찾습니다.
현재 반복만 끝내는 break, 현재 반복만 건너뛰는 continue, 함수 작업을 끝내는 return, helper 함수와 RAII로 범위를 좁힐 수 있는지 검토합니다.
goto의 핵심 위험은 소멸자를 무조건 막는 데 있지 않습니다. 허용된 점프라도 초기화와 부수 효과를 건너뛰어 제어 흐름을 숨길 수 있고, raw ownership처럼 수동 정리에 의존하는 코드에서는 누수 경로를 만들기 쉽다는 데 있습니다.