ベスト プラクティスと例 (SAL)

ソース コード注釈言語 (SAL) を最大限活用し、いくつかの一般的な問題を回避する方法を示します。

_In_

関数が要素に書き込むことになっている場合、_Inout__In_ の代わりに使用します。 これは、古いマクロから SAL への自動変換の場合に関連します。 SAL より前は、多くのプログラマがマクロをコメントとして使用していました。INOUTIN_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_ 注釈を使用することをお勧めします。 この "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 バイトが割り当て済みで、最初のバイトが終了時に初期化されています。 この注釈は厳密には間違いではなく、割り当てられたサイズを表すのに役立ちます。 ただし、関数が初期化する要素の数は示されません。

次の例は、バッファーの初期化された部分の正確なサイズを完全に指定する、3 種類の正しい方法を示しています。

#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 のような注釈が一般的で便利です。 この注釈は、_In_ の事前条件によって NULL で終わる文字列を認識できるので、NULL 終了の入力文字列を指します。

_In_ WCHAR* p

_In_ WCHAR* p は、1 文字を指す入力ポインター 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
);

_Out_writes_to_(cbSize,*pcbFilled) から推察できるので _Deref_out_range_(0, cbSize) は一部のツールでは厳密には必須ではありませんが、完全を期してここに示しています。

_When_ の間違ったコンテキスト

もう 1 つのよくある間違いは、事後状態の評価を事前条件に使用することです。 次の例で、_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 != 0return == TRUE の代わりに成功条件として使用します。 0 以外の値は、コンパイラが TRUE に提供する実際の値と等価であるとは限りません。 _Success_ に対するパラメーターは式で、return != 0return != falsereturn != 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 について
関数パラメーターおよび戻り値の注釈設定
関数の動作に注釈を付ける
構造体とクラスに注釈を付ける
ロック動作に注釈を付ける
注釈を適用するタイミングと場所の指定
組み込み関数