다음을 통해 공유


모범 사례 및 예제(SAL)

SAL(Source Code Annotation Language)을 최대한 활용하고 몇 가지 일반적인 문제를 방지하는 방법은 다음과 같습니다.

_In_

함수가 요소에 항목을 기록하는 경우 _Inout_ 대신 _In_을 사용합니다. 이는 이전 매크로에서 SAL로 자동 변환하는 경우와 관련이 있습니다. SAL 이전에는 많은 프로그래머들이 매크로를 주석으로 사용했습니다. 이러한 매크로에는 주로 IN, OUT, IN_OUT 등의 이름이 사용되었습니다. 이러한 매크로를 SAL로 변환하는 것이 좋지만 원래 프로토타입이 작성된 이후 코드가 변경되었을 수 있고 이전 매크로가 더 이상 코드 수행 작업을 반영하지 않을 수 있기 때문에 매크로를 변환할 때에도 특별한 주의가 필요할 수 있습니다. 메모 매크로는 OPTIONAL 쉼표의 잘못된 쪽과 같이 잘못 배치되는 경우가 많기 때문에 특히 주의해야 합니다.

#include <sal.h>

// Incorrect
void Func1(_In_ int *p1)
{
    if (p1 == NULL)
        return;

    *p1 = 1;
}

// Correct
// _Out_opt_ because the function tolerates NULL as a valid argument, i.e.
// no error is returned. If the function didn't check p1 for NULL, then
// _Out_ would be the better choice
void Func2(_Out_opt_ PCHAR p1)
{
    if (p1 == NULL)
        return;

    *p1 = 1;
}

_opt_

호출자가 null 포인터를 전달할 수 없으면 사용 _In_ 하거나 _Out_ _In_opt_ 대신 사용합니다 _Out_opt_. 이는 해당 매개 변수를 확인하고 그렇지 않은 경우 NULL 오류를 반환하는 함수에도 적용됩니다. 함수가 예기치 않은 NULL 매개 변수를 확인하고 정상적으로 반환하는 것은 좋은 방어 코딩 연습이지만 매개 변수 주석이 선택적 형식(_*Xxx*_opt_)일 수 있다는 의미는 아닙니다.

#include <sal.h>

// Incorrect
void Func1(_Out_opt_ int *p1)
{
    *p = 1;
}

// Correct
void Func2(_Out_ int *p1)
{
    *p = 1;
}

_Pre_defensive__Post_defensive_

함수가 신뢰 경계에 나타날 경우에는 _Pre_defensive_ 주석을 사용하는 것이 좋습니다. "방어적" 수정자는 호출 시점에 인터페이스가 엄격하게 검사되도록 특정 주석을 수정하지만, 구현 본문에서는 잘못된 매개 변수가 전달될 수 있다고 가정해야 합니다. 이 경우 _In_ _Pre_defensive_ 호출자가 전달 NULL하려고 하면 오류가 발생하지만 함수 본문은 매개 변수가 될 NULL수 있는 것처럼 분석되고 포인터를 먼저 확인하지 않고 역참조하려는 시도는 플래그가 지정됨을 나타내기 위해 NULL 트러스트 경계에서 선호됩니다. 신뢰할 수 있는 당사자가 호출자인 것으로 간주되고 신뢰할 수 없는 코드가 호출된 코드인 콜백에서는 _Post_defensive_ 주석도 사용할 수 있습니다.

_Out_writes_

다음 예제는 일반적으로 _Out_writes_의 잘못 사용된 경우를 보여줍니다.

#include <sal.h>

// Incorrect
void Func1(_Out_writes_(size) CHAR *pb,
    DWORD size
);

_Out_writes_ 주석은 버퍼가 있어야 한다는 것을 나타냅니다. 여기에는 종료 시 초기화되는 첫 번째 바이트와 함께 cb 바이트가 할당됩니다. 이 주석은 엄밀히 말하면 잘못되지 않으며 할당된 크기를 표현하는 것이 유용합니다. 그러나 함수가 초기화하는 요소의 수는 알려주지 않습니다.

다음 예제에서는 버퍼의 초기화된 부분에 대한 정확한 크기를 완전하게 지정하는 세 가지 올바른 방법을 보여줍니다.

#include <sal.h>

// Correct
void Func1(_Out_writes_to_(size, *pCount) CHAR *pb,
    DWORD size,
    PDWORD pCount
);

void Func2(_Out_writes_all_(size) CHAR *pb,
    DWORD size
);

void Func3(_Out_writes_(size) PSTR pb,
    DWORD size
);

_Out_ PSTR

_Out_ PSTR 사용은 항상 거의 잘못된 것입니다. 이 조합은 문자 버퍼를 가리키는 출력 매개 변수가 있고 버퍼가 null로 종료되는 것으로 해석됩니다.

#include <sal.h>

// Incorrect
void Func1(_Out_ PSTR pFileName, size_t n);

// Correct
void Func2(_Out_writes_(n) PSTR wszFileName, size_t n);

_In_ PCSTR과 같은 주석은 일반적이고 유용합니다. null 종료 문자열의 사전 조건을 통해 null로 끝나는 문자열을 _In_ 인식할 수 있기 때문에 null 종료가 있는 입력 문자열을 가리킵니다.

_In_ WCHAR* p

_In_ WCHAR* p 는 한 문자를 가리키는 입력 포인터 p 가 있음을 나타냅니다. 하지만 대부분의 경우에는 의도된 사양이 아닙니다. 대신 null로 끝나는 배열의 사양이 의도된 것일 수 있습니다. 이렇게 하려면 .를 사용합니다 _In_ PWSTR.

#include <sal.h>

// Incorrect
void Func1(_In_ WCHAR* wszFileName);

// Correct
void Func2(_In_ PWSTR wszFileName);

Null 종료의 적절한 사양이 누락되는 것이 일반적입니다. 다음 예제에 표시된 것처럼 적절한 STR 버전을 사용해서 형식을 바꾸십시오.

#include <sal.h>
#include <string.h>

// Incorrect
BOOL StrEquals1(_In_ PCHAR p1, _In_ PCHAR p2)
{
    return strcmp(p1, p2) == 0;
}

// Correct
BOOL StrEquals2(_In_ PSTR p1, _In_ PSTR p2)
{
    return strcmp(p1, p2) == 0;
}

_Out_range_

매개 변수가 포인터이고 포인터가 가리키는 요소 값의 범위를 표현하려는 경우 대신 사용합니다 _Deref_out_range_ _Out_range_. 다음 예제에서는 pcbFilled가 아니라 *pcbFilled의 범위가 표시됩니다.

#include <sal.h>

// Incorrect
void Func1(
    _Out_writes_bytes_to_(cbSize, *pcbFilled) BYTE *pb,
    DWORD cbSize,
    _Out_range_(0, cbSize) DWORD *pcbFilled
);

// Correct
void Func2(
    _Out_writes_bytes_to_(cbSize, *pcbFilled) BYTE *pb,
    DWORD cbSize,
    _Deref_out_range_(0, cbSize) _Out_ DWORD *pcbFilled
);

_Deref_out_range_(0, cbSize) 에서 유추 _Out_writes_to_(cbSize,*pcbFilled)할 수 있으므로 일부 도구에 반드시 필요한 것은 아니지만 완전성을 위해 여기에 표시됩니다.

의 잘못된 컨텍스트 _When_

또 다른 일반적인 실수는 사전 조건을 위해 사후 상태 평가를 사용하는 것입니다. 다음 예제에서 _Requires_lock_held_는 사전 조건입니다.

#include <sal.h>

// Incorrect
_When_(return == 0, _Requires_lock_held_(p->cs))
int Func1(_In_ MyData *p, int flag);

// Correct
_When_(flag == 0, _Requires_lock_held_(p->cs))
int Func2(_In_ MyData *p, int flag);

return 은 사전 상태에서 사용할 수 없는 사후 상태 값을 나타냅니다.

_Success_TRUE

반환 값이 0이 아닐 때 함수가 성공하면 return != 0 대신 return == TRUE을 성공 조건으로 사용합니다. 0이 아니어도 컴파일러가 제공하는 TRUE실제 값과 동등함을 의미하지는 않습니다. _Success_에 대한 매개 변수는 식이고, return != 0, return != false, return != FALSE 및 매개 변수 또는 비교가 없는 return과 같은 식이 동일 항목으로 평가됩니다.

// Incorrect
_Success_(return == TRUE) _Acquires_lock_(*lpCriticalSection)
BOOL WINAPI TryEnterCriticalSection(
  _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

// Correct
_Success_(return != 0) _Acquires_lock_(*lpCriticalSection)
BOOL WINAPI TryEnterCriticalSection(
  _Inout_ LPCRITICAL_SECTION lpCriticalSection
);

참조 변수

참조 변수의 경우 이전 버전의 SAL에서는 주석 대상으로 암시적 포인터가 사용되었고 참조 변수에 연결되는 주석에 __deref를 추가해야 했습니다. 이 버전은 개체 자체를 사용하며 필요하지 _Deref_않습니다.

#include <sal.h>

// Incorrect
void Func1(
    _Out_writes_bytes_all_(cbSize) BYTE *pb,
    _Deref_ _Out_range_(0, 2) _Out_ DWORD &cbSize
);

// Correct
void Func2(
    _Out_writes_bytes_all_(cbSize) BYTE *pb,
    _Out_range_(0, 2) _Out_ DWORD &cbSize
);

반환 값에 대한 주석

다음 예제에서는 반환 값 주석의 일반적인 문제를 보여줍니다.

#include <sal.h>

// Incorrect
_Out_opt_ void *MightReturnNullPtr1();

// Correct
_Ret_maybenull_ void *MightReturnNullPtr2();

이 예제 _Out_opt_ 에서는 포인터가 전제 조건의 일부일 NULL 수 있다고 말합니다. 그러나 반환 값에는 사전 조건을 적용할 수 없습니다. 이 경우에 올바른 주석은 _Ret_maybenull_입니다.

참고 항목

SAL 주석을 사용하여 C/C++ 코드 결함 줄이기
SAL 이해
함수 매개 변수에 주석을 추가하고 값을 반환합니다.
함수 동작에 주석 추가
구조체 및 클래스에 주석 추가
잠금 동작에 주석 추가
주석이 적용되는 시기 및 위치 지정
내장 함수