C 프로그래밍에서 엄격한 앨리어싱(strict aliasing) 규칙은 왜 지켜야 할까요? 이를 이해하기 위해, 먼저 간단한 예제 코드를 살펴보고 그 출력 결과를 분석해 보겠습니다.
예제 코드
#include<stdio.h>
int temp = 5;
int* var = &temp;
int my_function(double* var) {
temp = 1;
*var = 5.10; // 이 연산이 temp의 값을 변경하게 됩니다
return (temp);
}
main() {
printf("%d", my_function((double*)&temp));
}출력 결과
1717986918
결과 분석
코드의 의도대로라면 my_function을 호출하면 1이 반환되어야 합니다. 함수 내부에서 temp = 1;을 실행한 직후 return (temp);로 값을 돌려주기 때문입니다.
그러나 실제 출력 결과는 1717986918이라는 전혀 다른 값입니다. 원인은 my_function((double*)&temp)처럼 int형 변수의 주소를 double* 포인터로 강제 형변환하여 접근했기 때문입니다. 이는 C 표준의 엄격한 앨리어싱 규칙을 위반하는 행위이며, 컴파일러는 서로 다른 타입의 객체가 같은 메모리를 공유하지 않는다고 가정하고 최적화를 수행합니다. 그 결과 프로그램의 동작은 정의되지 않은 동작(undefined behavior)이 되고, 예상과 다른 값이 반환됩니다.
해결 방법
이 문제를 해결하는 가장 확실한 방법은 엄격한 앨리어싱 규칙을 준수하는 것입니다. 즉, 한 타입으로 생성된 객체에 다른 타입의 포인터로 접근하지 않아야 합니다. 타입 변환이 꼭 필요하다면 memcpy나 공용체(union)를 활용하는 것이 안전합니다.
또한 컴파일 시 -fno-strict-aliasing 옵션을 사용하여 엄격한 앨리어싱 기반 최적화를 비활성화할 수도 있지만, 이는 성능 저하를 감수하는 임시방편일 뿐 근본적인 해결책은 아닙니다.
참고로 restrict 한정자(qualifier) 키워드도 관련된 개념입니다. 이 키워드는 해당 포인터가 가리키는 객체가 다른 포인터와 앨리어싱(별칭)되지 않음을 컴파일러에게 약속하는 것입니다. 만약 이 약속을 어기고 실제로 별칭이 존재한다면, 최적화된 코드에서 예기치 못한 오류가 발생할 수 있습니다.