본문 바로가기
C++

[C++ Core Guidelines] P.6 컴파일 타임에 평가가 불가능한 코드는 런타임에서라도 평가되도록 하자

by 코드쉼터 2024. 4. 11.

P.6: What cannot be checked at compile time should be checkable at run time

 

이유

런타임에만 감지 가능한 에러들은 보통 가끔씩만 발생하거나 감지하기가 까다로운 경우가 많습니다.

이러한 에러가 발생하도록 그냥 놔두면 프로그램 실행 중 충돌이 발생하고 나쁜 결과를 초래합니다.

 

 

알아두기

네.. 런타임에 남아 있는 모든 오류를 포착하는 것은 종종 감당하기 힘든 일입니다. 그러나 충분한 리소스(분석 프로그램, 런타임 검사, 머신 레벨 리소스, 시간)가 주어지면 원칙적으로 모든 에러를 확인할 수 있게끔 프로그램을 작성하도록 노력해야 합니다.

 

예시 (나쁜 예)

// f() 가 별도로 컴파일되고 동적으로 로드될 수도 있는 함수라고 해봅시다
extern void f(int* p);

void g(int n)
{
    // 나쁨 : f() 를 호출하면서 배열의 크기를 전달하고 있지 않습니다
    f(new int[n]);
}

 

배열을 생성할 때 사용하는 크기 n 이 철저하게 "모호"하여 (예를들어 사용자의 입력을 받아 결정되는 경우) 정적 분석이 불가능할 수 있으며 f()가 ABI(Application Binary Interface) 의 일부인 경우 동적 검사 또한 어려워서 평가 될 수 없는 상황입니다. 배열 크기 정보를 전역적으로 저장하고 관리할 수 있지만 그러려면 시스템이나 컴파일러 레벨에서 대대적 수정이 필요합니다. 이것이 오류 감지를 매우 어렵게 만드는 이유입니다.

 

예시 (나쁜 예)

// f2() 는 별도로 컴파일되고 동적으로 로드될 수도 있는 함수입니다
extern void f2(int* p, int n);

void g2(int n)
{
	// 여전히 나쁨 : 잘못된 배열의 크기가 m 을 통해 전달될 수 있습니다
    f2(new int[n], m);
}

배열 크기를 인자로 전달하는 것은 단순히 포인터만 던져버리고 자체적인 규칙이나 전역으로 저장한 정보에 의존하는 것보단 더 낫고 훨씬 더 일반적입니다. 그러나 (표시된 것처럼) 간단한 오타로 인해 심각한 오류가 발생할 수 있습니다.
또한 f2()가 동적으로 할당된 배열 (힙 메모리) 를 알아서 삭제해주는 것인지..? 소유권도 모호합니다.

 

 

예시 (여전히 나쁜 예)

// f3() 는 별도로 컴파일되고 동적으로 로드될 수도 있는 함수입니다
// 호출 코드는 바이너리 수준에서 호환된다고 가정합니다 (C++ 컴파일러와 stdlib 호환성 보장)
extern void f3(unique_ptr<int[]>, int n);

void g3(int n)
{
    // 나쁨 : 소유권과 크기를 별도로 전달합니다
    f3(make_unique<int[]>(n), m);
}

배열 크기를 인자로 전달하는 것은 단순히 포인터를 전달하고 자체적인 규칙이나 전역으로 저장한 정보에 의존하는 것보단 훨씬 더 낫고 더 일반적입니다. 그러나 (표시된 것처럼) 간단한 오타로 인해 심각한 오류가 발생할 수 있습니다.
또한 f2()가 동적으로 할당된 배열 (힙 메모리) 를 알아서 삭제해주는 것인지도 모호합니다.

 

 

 

예시 (좋은 예)

extern void f4(vector<int>&);   // separately compiled, possibly dynamically loaded
extern void f4(span<int>);      // separately compiled, possibly dynamically loaded
                                // NB: this assumes the calling code is ABI-compatible, using a
                                // compatible C++ compiler and the same stdlib implementation

void g3(int n)
{
    vector<int> v(n);
    f4(v);                     // pass a reference, retain ownership
    f4(span<int>{v});          // pass a view, retain ownership
}

 

이렇게 작성하면 개체의 일부분으로써 여러 평가 정보 (배열 크기 등) 이 함께 전달되므로 오류 평가가 쉽고 동적(런타임) 검사가 가능합니다.

 

 

예시 (좋은 예, 나쁜 예)

어떻게 소유권과 평가 정보를 다른 함수에 전달해야 잘했다고 소문날까요?

vector<int> f5(int n)    // 좋음 : 소유권 move 가 가능하고 vector 개체는 크기 정보도 스스로 가지고 있습니다
{
    vector<int> v(n);
    // ... initialize v ...
    return v;
}

unique_ptr<int[]> f6(int n)    // 나쁨 : 반환시 n 정보를 잃습니다
{
    auto p = make_unique<int[]>(n);
    // ... initialize *p ...
    return p;
}

owner<int*> f7(int n)    // 나쁨 : 반환시 n 정보도 잃고 delete 하는걸 까먹을 수도 있습니다
{
    owner<int*> p = new int[n];
    // ... initialize *p ...
    return p;
}

 

 

정리

1. dynamic_cast 를 사용해서 포인터, 레퍼런스 또는 클래스 부모 자식 관계를 안전하게 변환하세요.

 (포인터 형변환 실패시 nullptr 가 반환되고 레퍼런스는 std::bad_cast 예외가 발생합니다.)

2. 포인터와 배열의 크기와 같은 정보는 std::span 이나 std::vector 와 같은 인터페이스로 한번에 전달하세요.

3. 소유권의 이동을 명확히 보여주세요.

 

 

참고자료

https://en.cppreference.com/w/cpp/header/type_traits