아, 표준 C에서는 문자열의 부동소수점 변환 오류를 이식성 있게 검사할 수 없는 모양이다
요약
본 글은 표준 C에서 문자열을 부동소수점으로 변환할 때 발생하는 오류 처리의 복잡성과 이식성 문제를 다룹니다. 특히, POSIX는 표준 C와 달리 오버플로/언더플로 발생 시 `errno`를 `ERANGE`로 설정하도록 요구하며, 이를 통해 개발자가 보다 강력한 오류 검출 방법을 사용할 수 있음을 설명합니다.
핵심 포인트
- 표준 C의 문자열 변환 함수는 오류 보고 방식이 구현마다 달라 이식성이 떨어집니다.
- POSIX 표준은 오버플로/언더플로 시 `errno`를 `ERANGE`로 설정하도록 강제하여 안정성을 높였습니다.
- 오류 검출을 위해서는 단순히 `errno`만 확인하는 것이 아니라, 부동소수점 예외 및 컴파일러 설정을 종합적으로 고려해야 합니다.
표준 C의 문자열→부동소수점 변환 함수는 math_errhandling에 따라 오류 보고 방식이 달라지며, 언더플로 보고는 선택 사항이라 모든 구현에서 검출을 보장할 수 없음
POSIX는 표준 C와 달리 오버플로와 언더플로 발생 시 math_errhandling과 관계없이 errno를 ERANGE로 설정하도록 요구함
숫자로 변환할 수 없는 입력은 endptr == nptr 로 확인해야 하며, errno만 검사하면 POSIX 호환 구현 사이에서도 결과가 달라짐
strtod("x", nullptr) 호출 전 errno를 0으로 초기화하면 glibc는 0, musl은 EINVAL에 해당하는 22를 남기며, Linux 매뉴얼은 이 차이를 문서화하지 않음
오류 검사에서는 대상 표준과 구현의 보장을 구분해야 하며, errno를 ERANGE와 명시적으로 비교하고 부동소수점 예외를 사용할 때는 컴파일러 설정도 확인해야 함
math_errhandling은 문자열 변환 함수에도 적용됨
앞선 글은 math_errhandling 과 glibc/musl의 수학 오류 처리, 표준의 요구 사항을 살펴봄
이 매크로는 math.h 함수가 errno 방식인 MATH_ERRNO, 부동소수점 예외 방식인 MATH_ERREXCEPT 중 무엇을 지원하는지 나타냄
math.h에 속하지 않는 strtod, strtof, strtold, strtod32, strtod64, strtod128 도 이 매크로의 영향을 받음
표준 C 7.25.2.6p12는 기본 반올림 모드에서 오버플로가 발생할 때 다음 동작을 요구함
반환형과 부호에 따라 ±HUGE_VAL, ±HUGE_VALF, ±HUGE_VALL을 반환함
math_errhandling & MATH_ERRNO가 0이 아니면 errno를 ERANGE로 설정함
math_errhandling & MATH_ERREXCEPT가 0이 아니면 오버플로 부동소수점 예외를 발생시킴
언더플로가 발생하면 반환값의 절댓값은 해당 반환형의 최소 양의 정규화 수 이하여야 하지만, 오류 보고 여부는 구현 정의 사항임
MATH_ERRNO를 지원하더라도 ERANGE 설정 여부는 구현에 달려 있음
MATH_ERREXCEPT를 지원하더라도 언더플로 예외 발생 여부는 구현에 달려 있음
반환값만으로는 오류를 판별할 수 없으므로, 오버플로 검출에는 두 오류 보고 방식 중 하나가 필요함
호출 전에 feclearexcept(FE_OVERFLOW | FE_UNDERFLOW)와 errno = 0을 수행하고, 호출 후 math_errhandling에 따라 errno == ERANGE 또는 fetestexcept(FE_OVERFLOW | FE_UNDERFLOW)를 검사할 수 있음
이 절차로도 선택 사항인 언더플로 보고까지 보장하지는 못함
POSIX의 요구 사항과 표준 C의 모호한 경계
POSIX는 표현 가능 범위를 벗어나면 부호에 맞는 HUGE_VAL 계열 값을 반환하고 errno를 ERANGE로 설정하도록 요구함
언더플로에서도 최소 양의 정규화 수 이하의 크기를 반환하고 errno를 ERANGE로 설정해야 함
이 요구는 math_errhandling과 무관함
Linux 매뉴얼은 이러한 차이를 다루지 않으며, strtod(3)과 strtod(3p) 모두 이를 표준 C보다 강화된 요구 사항으로 별도 표시하지 않음
POSIX 동작이 표준 C와 양립하는지는 불명확함
math.h에 관한 7.12.2p8은 도메인 오류, 극점 오류, 범위 오류가 발생하면 MATH_ERRNO가 꺼져 있어도 errno를 설정하거나 그대로 둘 수 있도록 허용함
반면 errno.h에 관한 7.5p3은 함수 설명에 errno 사용이 문서화되어 있지 않은 경우, 오류 유무와 관계없이 라이브러리 함수가 errno를 0이 아닌 값으로 설정할 수 있다고 규정함
문자열 변환 함수에는 errno 사용이 이미 문서화되어 있지만, MATH_ERRNO가 꺼진 경우의 설정은 명시하지 않으므로 POSIX 동작이 비준수라는 해석의 여지가 있음
잘못된 입력은 errno가 아니라 endptr로 확인
표준 C 7.25.2.6p11은 변환할 수 없으면 양의 0 또는 부호 없는 0을 반환하도록 하며, 이를 별도의 오류 조건으로 규정하지 않음
7.25.2.6p8에 따라 입력이 비어 있거나 기대한 형식이 아니면 변환하지 않고, endptr가 널 포인터가 아닐 때 그 대상에 입력 시작 포인터 nptr를 저장함
따라서 endptr == nptr 이면 아무것도 변환하지 못한 것임
Linux의 strtod(3)도 같은 반환값과 포인터 동작을 안내함
POSIX는 변환 실패 시 0을 반환하되, errno를 EINVAL로 설정하는 것은 허용 사항으로 둠
따라서 errno = 0으로 초기화한 뒤 errno != 0만 검사하는 방식은 POSIX 호환 라이브러리 사이에서도 이식성이 없음
그런데 strtod(3)은 성공과 실패 모두 0을 반환할 수 있다는 이유로 바로 이 검사 방식을 권장함
errno = 0 이후 strtod("x", nullptr) 를 호출하면 구현별 차이가 드러남
glibc의 strtod는 EINVAL을 설정하지 않아 errno가 0으로 남음
musl은 EINVAL을 설정하므로 예제의 출력값이 22가 됨
Linux 매뉴얼에는 이 차이가 문서화되어 있지 않음
musl의 동작 역시 표준 C 비준수로 읽힐 여지가 있음
표준 C에서 잘못된 입력은 오류 조건이 아닌데, 다른 오류 조건에 대한 errno 사용이 문서화된 함수가 0이 아닌 값을 설정하기 때문임
musl이 math.h 함수에서 errno를 사용하지 않으므로 strtod의 조건부 errno 문서화도 적용되지 않는다는 반론은 가능하지만, 무리한 해석에 가까움
이 경계에는 더 명확한 표준 문구가 필요함
오버플로 검사 절차
POSIX 대상이라면 호출 전 errno = 0으로 초기화하고, 호출 후 copysign(result, 1.0) == HUGE_VAL && errno == ERANGE를 검사함
표준 C만 전제한다면 호출 전 errno = 0과 feclearexcept(FE_OVERFLOW)를 모두 수행함
호출 후 copysign(result, 1.0) == HUGE_VAL이면, math_errhandling에 따라 errno == ERANGE 또는 fetestexcept(FE_OVERFLOW)를 검사함
하나의 구현만 대상으로 하고 지원하는 오류 보고 방식을 미리 안다면 math_errhandling 검사와 사용하지 않는 방식은 생략할 수 있음
부동소수점 예외를 검사할 때는 컴파일러가 부동소수점 환경 접근을 고려하도록 해야 함
GCC의 관련 옵션은 -ftrapping-math이며, -ffast-math를 사용하지 않는 경우 기본으로 활성화됨
표준 지시문으로 #pragma STDC FENV_ACCESS ON도 있지만 GCC는 이를 지원하지 않음
언더플로 검사와 남는 한계
POSIX를 대상으로 한 검사 예는 호출 전 errno = 0, 호출 후 result == 0.0 && errno == ERANGE 임
errno가 단순히 0이 아닌지 확인하지 말고 반드시 ERANGE와 비교해야 구현별 차이를 피할 수 있음
POSIX가 아닌 단일 구현을 대상으로 한다면 언더플로 보고 동작의 문서화 여부를 확인해야 함
구현 정의 사항은 문서화해야 하지만 실제 문서는 불완전할 수 있으며, Clang도 구현 정의 사항을 충실히 문서화하지 않는 사례임
언더플로를 보고하는 구현이라면 math_errhandling에 맞춰 errno 또는 부동소수점 예외를 검사함
errno 방식은 호출 전 0으로 초기화하고 호출 후 result == 0.0 && errno == ERANGE를 확인함
예외 방식은 호출 전 feclearexcept(FE_UNDERFLOW), 호출 후 fetestexcept(FE_UNDERFLOW)를 사용함
이러한 보장이 없으면 이식성 있는 언더플로 검출을 보장할 수 없음
libc 함수를 반드시 사용하면서 언더플로를 확인해야 할 때 생각해 볼 수 있는 대안은 입력 문자열 직접 검사임
result == 0.0이면 지수 앞부분에 0이 아닌 숫자가 있는지 직접 확인하고, 있다면 언더플로로 판단함
올바르게 구현하기 어려우므로 strtod 계열의 표준 설명을 읽고 모든 경계 사례를 처리해야 함
변환 자체가 이루어졌는지 확인하는 방법은 별개로 명확함. 두 번째 인수에 &endptr 를 전달하고 endptr == nptr를 검사해야 하며, errno에 의존해서는 안 됨
Python 같은 고수준 언어로 작업하다 지치면 기계에 더 가까운 저수준 언어를 써 보고 싶어짐. Go와 Java는 너무 기업 취향이고, Zig·Odin·Hare는 너무 유행을 타고, Rust는 너무 산업적이고, C++는 너무 러브크래프트식 공포에 가까움.
그래서 C를 떠올림. 50년이나 됐고 작고 단순하니 이제는 완벽하게 다듬어졌겠지 싶다가도, 이런 글을 읽게 됨. Forth를 더 알아봐야 할지도 모르겠음.
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기