C++ テンプレート関数の呼び出し間違いをコンパイル時に検出する方法
はじめに
今、次のようなテンプレート関数があるとしましょう
このテンプレート関数には問題があります
template<typename pointer_type_> inline void my_delete( pointer_type_& p ){ if( p ){ delete p; p = NULL; } } template<typename pointer_type_> inline void my_delete_array( pointer_type_& p ){ if( p ){ delete[] p; p = NULL; } }
delete/delete[] を呼んでポインタ変数を NULL にしてくれるだけの簡単な関数なので、何も問題は無さそうに見えます
しかし問題があります
delete/delete[] に void* を渡すことはできない
標準に次のような記述があります
5. Expressions 5.3. Unary expressions 5.3.5 Delete 1 The delete-expression operator destroys a most derived object (1.8) or array created by a new-expression. delete-expression: ::opt delete cast-expression ::opt delete [ ] cast-expression The first alternative is for non-array objects, and the second is for arrays. Whenever the delete keyword is immediately followed by empty square brackets, it shall be interpreted as the second alternative.(76 ---- 76) ... The operand shall have a pointer to object type, or a class type having a single non-explicit conversion function (12.3.2) to a pointer to object type. The result has type void.(77 77) This implies that an object cannot be deleted using a pointer of type void* because void is not an object type.
void* を delete/delete[] に渡すことはできません
void* を検出できるようにしましょう
void* が渡されたらエラーにする
単純に void* の場合を特殊化すれば確かに検出はできます
// void* の delete/delete[] は規格違反 template<> inline void my_delete<void*>( void*& p ); template<> inline void my_delete_array<void*>( void*& p );
しかし、実体のない関数の宣言というのはコンパイル時には合法なので、問題なくコンパイルは通ってしまいます
リンク時になると実体のないものはエラーですから、ここでようやくエラーが検出できることになります
これではちょっと困りますね
コンパイル時に検出する方法はないでしょうか
渡されたものの型が void* だったらエラーにする
「void* が渡されたら」というのは言い換えれば「渡されたものの型が void* だったら」ということです
「型が○○だったら」とテンプレートで場合分けをしたい場合はそこを別のテンプレートに追い出すのでした
struct false_type { static const bool value = false; }; struct true_type { static const bool value = true; }; template<typename pointer_type_> struct is_void_pointer : public false_type {}; template<> struct is_void_pointer<void*> : public true_type {}; template<typename pointer_type_> inline void my_delete( pointer_type_& p ){ static_assert( is_void_pointer<T>::value, "attempt to delete void*" ); if( p ){ delete p; p = NULL; } }
これでコンパイル時に問題を解決できるようになりました
型で場合分けをしたいときはそこを別のテンプレートに追い出す
型で場合分けをしたいときはそこを別のテンプレートに追い出す。です
static_assert を使わない
上記で問題をコンパイル時に検出することはできるようになったのですが、何もわざわざ static_assert を使わなくてももっと簡単な方法があります
それはテンプレート関数の実体をテンプレートクラスに移すことです
template<typename pointer_type_> struct my_delete_t { static void my_delete( pointer_type_& p ){ if( p ){ delete p; p = NULL; } } static void my_delete_array( pointer_type_& p ){ if( p ){ delete[] p; p = NULL; } } }; // void* の delete/delete[] は規格違反 template<> struct my_delete_t<void*> {}; template<typename pointer_type_> inline void my_delete( pointer_type_& p ){ my_delete_t<pointer_type_>::my_delete( p ); } template<typename pointer_type_> inline void my_delete_array( pointer_type_& p ){ my_delete_t<pointer_type_>::my_delete_array( p ); }
my_delete( p ) に void* を渡すと my_delete_t
関数の実体があるかないかはリンク時までわかりませんが、関数の宣言があるかどうかはコンパイル時に分かることなのですぐにエラーにできます
テンプレート関数の実体はテンプレートクラスに
基本的にテンプレート関数の実体はテンプレートクラスの static メンバ関数にしておくといいと思います
scalar を指すポインタを delete しようとしているのは何かおかしい
ところで char* や int* などを delete するでしょうか
char* s = new ...; ... delete s;
一見問題なさそうに見えますが、実はこれはおかしなことです
char* を delete して問題ないケースというのは次のようなメモリ確保方法だったときですね
char* s = new char; ... delete s;
char を一つだけ new するということがあるでしょうか
普通は配列で確保するものではないでしょうか
char* s = new char[...]; ... delete[] s;
このような問題を検出できるようにしてみてもいいでしょう