본문 바로가기
C++

[C++ Core Guidelines] I.7, I.8 완료 조건을 명시하자

by 코드쉼터 2024. 5. 27.

I.7: State postconditions

I.8: Prefer Ensures() for expressing postconditions

 

이유

실수로 잘못된 결과를 반환하는 것을 막을 수 있습니다.

 

 

예시 (나쁜 예)

int area(int height, int width) { return height * width; }  // 나쁨

사각형의 넓이를 구하는 함수를 만들었습니다.

다만 전제 조건 검사를 생략했기 때문에 height 와 width 값으로 음수가 들어올 수도 있습니다.

그것 뿐인가요? height * width 결과가 int 로 표현할 수 있는 최대값 보다 크다면 오버플로우도 발생합니다.

 

 

예시 (좋은 예)

int area(int height, int width)
{
    auto res = height * width;
    Ensures(res > 0);			// 반환값이 양수임을 보장합니다
    return res;
}

 

 

예시 (나쁜 예)

void f()    // 문제가 있습니다
{
    char buffer[MAX];
    // ...
    memset(buffer, 0, sizeof(buffer));
}

보안 문제를 발생시키는 유명한 코드입니다. (스택 오버플로우)

buffer[] 는 지역 변수이고 함수를 나갈때 memset 으로 메모리를 0으로 초기화 하는 것은 분명 불필요하기 때문에

컴파일러는 최적화 옵션에 따라 이 코드를 지워버릴 것입니다.

 

 

예시 (좋은 예)

void f()    // 더 나은 코드
{
    char buffer[MAX];
    // ...
    memset(buffer, 0, sizeof(buffer));
    Ensures(buffer[0] == 0);
}

 

 

임베디드 시스템과 같이 스택 메모리를 임의로 0으로 초기화해야 하는 경우도 있습니다.

특정 메모리 상태를 확인하는 코드나 이 메모리를 사용하는 코드가 memset 이후에 존재한다면

컴파일러는 성능을 향상시키기 위해 memset과 같은 코드를 임의로 제거할 수 없습니다.

이때 Enaches() 를 사용하면 완료 조건을 명시하면 컴파일 타임에 검사하고, 가독성도 좋아집니다.

 

 

알아두기

반환 조건은 자료구조의 상태처럼 반환값으로 결과가 직접 보여지지 않는 경우에 더욱 유용합다.

 

 

예시 (나쁜 예)

mutex m;

void manipulate(Record& r)    // don't
{
    m.lock();
    // ... no m.unlock() ...
}

경쟁 조건을 피하기 위해 뮤텍스를 사용하여 데이터를 조작하는 함수를 만들었습니다.
실수로 뮤텍스가 해제되어야 한다고 주석으로 명시하는 것을 까먹었습니다.

따라서 뮤텍스 해제를 보장하지 못한 것이 버그인지 기능인지 알 수 없습니다.

 

 

예시 (그럭저럭 괜찮은 예)

void manipulate(Record& r)    // 완료 조건 : 함수를 나갈때 m 을 꼭 unlock 시키세요!!
{
    m.lock();
    // ... no m.unlock() ...
}

완료 조건을 주석으로 명시해서 뮤텍스를 해제해야 하는 것을 명확하게 표현했습니다.

그렇지만 오직 사람만이 알아볼 수 있는 코드입니다.

주석에 주의를 기울이지 않는 사람도 많습니다.

 

 

예시 (좋은 예)

void manipulate(Record& r)    // 최고
{
    lock_guard<mutex> _ {m};
    // ...
}

RAII 기법은 완료 조건을 "보장" 합니다.

위처럼 코드를 작성하면 함수를 빠져 나갈 때 문법적으로 m 이 해제 될 것임을 보장할 수 있습니다.

 

 

알아두기

이상적으로 완료 조건은 사용자가 쉽게 볼 수 있도록 인터페이스/선언에 명시되어 있습니다.

인터페이스에는 사용자와 관련된 완 조건만 기술할 수 있습니다.

함수 내부에서만 필요한 조건 검사는 완료 조건이 아니라 정의/구현에 속합니다.

 

 

알아두기

일반적인 경우 위 가이드라인처럼 RAII 를 사용할 수 없는 경우도 있습니다.

이런 경우 lock 유지 검사 도구와 같이 특정 분야에서 많이 사용하는 툴체인에 검사를 의존해야 합니다.

 

 

알아두기

완료 조건도 전제 조건처럼 주석, if 문, assert() 등 다양한 방법으로 정의할 수 있습니다.

다만 일반 코드와 구별이 어렵고, 도구로 검사하기 어렵고, 의미가 잘못 전달될 위험이 있습니다.
"이 자원은 반드시 해제되어야 합니다" 와 같은 의미의 완료 조건은 RAII 에 의해 가장 잘 표현됩니다.

 

 

알아두기

이상적으로는 Ensures() 와 같은 기능이 인터페이스에 포함되었어야 합니다.

지금은 어쩔 수 없이 이를 정의 (함수 본문) 에 배치합니다.

언어 레벨에서의 지원이 가능해지면  우리는 preconditions, postconditions, assertions의 표준을 만들 것입니다.

 

 

정리

1. "이 자원은 반드시 해제되어야 합니다" 를 표현하고 싶은 경우 RAII 로 구현하세요.

2. 나머지 조건은 Ensures() 를 사용해서 검사하세요.

 

 

참고자료

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0380r1.pdf