모범 사례 및 예제(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 이해
함수 매개 변수에 주석을 추가하고 값을 반환합니다.
함수 동작에 주석 추가
구조체 및 클래스에 주석 추가
잠금 동작에 주석 추가
주석이 적용되는 시기 및 위치 지정
내장 함수