P.7: Catch run-time errors early
이유
런타임 에러를 빨리 잡지 않으면 나중에 나비효과로 "신비한" 충돌이 발생할 수 있고 충돌의 실제 원인을 파악하기 어렵게 만듭니다.
숙련된 C++ 개발자들은 객체의 생성자에 (이른 시점에) 유효한 값으로 객체를 초기화 하고 있는지 검사하는 것을 선호하기도 합니다.
이것이 항상 바람직한 방법인지, 주의해야 할 점은 없는지 알아봅시다.
예시 (나쁜 예)
void increment1(int* p, int n) // 나쁨 : 에러를 발생시키기 쉽습니다
{
for (int i = 0; i < n; ++i)
++p[i];
}
void use1(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment1(a, m); // 오타가 났나요? 아마 m <= n 임을 가정하고 이렇게 작성했을수도 있지만
// 실수로 m 이 20 이면 어떤 문제가 발생할까요?
// ...
}
increment1() 함수는 배열의 모든 요소를 1씩 증가시키는 목적을 가지고 있습니다만, 실수로 실제 배열의 길이보다 더 큰 n 값을 받더라도 방어할 수 있는 방법이 없습니다. 더 심각한 문제는 배열 크기를 넘어서는 접근을 시도할때까지 프로그램이 마치 정상적으로 수행되는 것 처럼 계속 동작한다는 것입니다.
예시 (좋은 예)
void increment2(span<int> p)
{
for (int& x : p) ++x;
}
void use2(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2({a, m}); // 어쩌면 오타거나 m <= n 임을 가정하고 작성한 것입니다
increment2(a); // 그냥 전체 범위에서 수행하고 싶으면 이렇게 전달해도 됩니다
// ...
}
이제 m <= n 은 increment2() 함수 호출 시점에 (정확히는 임시개체 대입시) 확인할 수 있습니다.
에러도 일찍 잡으면서, 코드를 더욱 단순화 화면서 범위 실수도 방지하는 일석이조 코드입니다.
예시 (나쁜 예)
// Date 생성자에는 날짜가 올바로 전달되는지 검사하는 코드가 있다고 가정합니다
Date read_date(istream& is); // istream 로 데이터를 읽습니다
Date extract_date(const string& s); // 문자열로부터 날짜를 추출합니다
void user1(const string& date)
{
auto d = extract_date(date); // (Date 생성자 호출 2)
// ...
}
void user2()
{
Date d = read_date(cin); // (Date 생성자 호출 1)
// ...
user1(d.to_string());
// ...
}
날짜가 올바른지 Date 생성자를 통해 2번 검증되지만, 애초에 동일한 값이었으므로 반복적으로 확인하여 성능을 낭비할 이유가 없습니다.
게다가 구조화된 데이터를 굳이 문자열로 바꿔서 전달하는 것은 꽤 비효율적인 방식입니다.
예시 (특수한 경우)
class Jet { // Physics says: e * e < x * x + y * y + z * z
float x;
float y;
float z;
float e;
public:
Jet(float x, float y, float z, float e)
:x(x), y(y), z(z), e(e)
{
// 여기서 값이 물리적으로 의미가 있는지 확인해야 합니까?
}
float m() const
{
// 아니면 여기서 확인하는게 좋을까요?
return sqrt(x * x + y * y + z * z - e * e);
}
???
}
과도한 검사는 비용이 많이 들 수 있습니다. 어차피 생성할 당시 값들에는 문제가 없다고 하더라도 실제 계산식으로 활용될 때 부동 소수점 오차 때문에 코드의 유효성을 보장받지 못하는 경우도 있습니다.
모든 물리적 계산식에서 값의 정밀도를 잃지 않는지 매번 개체를 생성할 때마다 미리 검사해 보는 등의 행위는 불필요합니다.
마찬가지로 인터페이스를 만들 때 점근적 알고리즘에 대한 유효성 검사를 추가하지 마세요. (예: 평균 복잡도가 O(1)인 인터페이스에 O(n) 시점까지 확인하는 검사를 추가하지 마세요)
정리
1. 항상 포인터와 배열의 범위를 주의 : 범위를 벗어나지 않도록 미리 검사하되, 반복해서 검사하지 마세요.
2. 형 변환 시 유의 : 정밀도나 메모리 크기가 줄어드는 변환인 경우 막거나, 불가피한 경우 경고 표시하세요.
3. 사용자로부터 입력받은 확인되지 않은 값들을 한번 체크하세요
4. String(문자열) 대신 구조체나 클래스를 사용해서 데이터를 표현하세요
'C++' 카테고리의 다른 글
[C++ 23] std::print, std::println 사용하기 (0) | 2024.04.15 |
---|---|
[C++ 20] 모듈 (Module) 사용하기 (0) | 2024.04.15 |
[C++ Core Guidelines] P.6 컴파일 타임에 평가가 불가능한 코드는 런타임에서라도 평가되도록 하자 (0) | 2024.04.11 |
[C++ Core Guidelines] P.5 가급적 컴파일 타임에 코드가 평가될 수 있도록 만들자 (0) | 2024.04.11 |
[C++ Core Guidelines] P.4 가급적 컴파일 타임 타입을 사용하자 (0) | 2024.04.07 |