C++ テンプレート引数に応じた場合分け
テンプレート内でのマルチバイト文字とワイド文字の扱いを例に、テンプレート引数に応じた場合分けを実現する方法について説明します
テンプレート引数に応じて何か処理を分けたいというときには、その場合分けを別のテンプレートに追い出すことで実現できます
テンプレート引数に応じた場合分け
今、Fig.1 のような関数を考えてみましょう
Fig.1
#define elementsizeof( a ) sizeof((a)[0]) #define elementsof( a ) (sizeof((a))/elemetsizeof((a))) void debug_dump_vectorA( const std::vector<std::string>& v, int indent = 0 ){ int n = 0; for( std::vector<std::string>::const_iterator p = v.begin(); p != v.end(); ++p ){ char s[100]; snprintf( s, elementsof( s ), "%*c[%d] ", indent, ' ', n ); OutputDebugStringA( s ); OutputDebugStringA( *p ); OutputDebugStringA( "\n" ); ++n; } }
この関数は std::string 専用になっているので、これを std::wstring にも対応したいとします
このようなときにどうしたらいいでしょうか
ここで Fig.2 のように std::wstring 専用のものを別にもう一つ書いても別に構わないのですが
Fig.2
void debug_dump_vectorW( const std::vector<std::wstring>& v, int indent = 0 ){ int n = 0; for( std::vector<std::wstring>::const_iterator p = v.begin(); p != v.end(); ++p ){ WCHAR s[100]; wsprintf( s, elementsof( s ), L"%*c[%d] ", indent, L' ', n ); OutputDebugStringW( s ); OutputDebugStringW( *p ); OutputDebugStringW( L"\n" ); ++n; } } #if !defined( UNICODE ) #define debug_dump_vector debug_dump_vectorA #else // UNICODE #define debug_dump_vector debug_dump_vectorW #endif // UNICODE
まったく同じものをもう一つ書くのもばかばかしいので普通はテンプレートにしようとするでしょう
Fig.3
template<class value_type> void debug_dump_vector( const std::vector<std::basic_string<value_type> >& v, int indent = 0 ){ int n = 0; for( std::vector<std::basic_string<value_type> >::const_iterator p = v.begin(); p != v.end(); ++p ){ value_type s[100]; _stprintf( s, _T("%*c[%d] "), indent, _T(' '), n ); OutputDebugString( s ); OutputDebugString( *p ); OutputDebugString( _T("\n")); ++n; } }
ただ、Fig.3 はこのままではコンパイルできません
だからといって、なんだじゃあ Fig.2 にするしかないじゃん。というわけでもありませんよ
がんばってテンプレートにしてみましょう
まず sprintf と OutputDebugString にはマルチバイト用とワイドキャラクタ用の2種類がありますが、それを明示しなかったときにこのどちらが呼ばれるかはコンパイルオプション(プリプロセッサシンボルの UNICODE が定義されているかどうか)によって決まっています
ですからこれはテンプレート引数の指定とは関係ありません
それでは困るのでこれをテンプレート引数の指定によって呼び分けられるようにしましょう
それにはこの2つの関数を外に追い出してそれぞれテンプレート関数とします
Fig.4
template<class T> inline void OutputDebugString( const std::basic_string<T>& s ){ return OutputDebugStringA( &s[0] ); } template<> inline void OutputDebugString<WCHAR>( const std::basic_string<WCHAR>& s ){ return OutputDebugStringW( &s[0] ); } template<class T> inline int snprintf( T* s, SIZE_T cch, const T* format,... ){ va_list ap; va_start( ap, format ); int result = vsnprintf( s, cch, format, ap ); va_end( ap ); return result; } template<> inline int snprintf<WCHAR>( WCHAR* s, SIZE_T cch, const WCHAR* format,... ){ va_list ap; va_start( ap, format ); int result = vswprintf( s, cch, format, ap ); va_end( ap ); return result; }
template<class T> void debug_dump_vector( const std::vector<std::basic_string<T> >& v, int indent = 0 ){ int n = 0; for( std::vector<std::basic_string<T> >::const_iterator p = v.begin(); p != v.end(); ++p ){ T s[100]; snprintf<T>( s, elementsof( s ), _T("%*c[%d] "), indent, _T(' '), n ); OutputDebugString<T>( s ); OutputDebugString<T>( *p ); OutputDebugString<T>( _T("\n")); ++n; } }
こうすることでテンプレート引数の指定によってマルチバイト用とワイドキャラクタ用の関数を呼び分けられるようになりました
しかし Fig.4 のようにしてもまだコンパイルは通りませんね
残った問題は Fig.4 の debug_dump_vector の中で使っている文字列リテラルと文字リテラルです
Fig.4 の debug_dump_vector の中で使っている文字列リテラルと文字リテラルには _T() マクロを適用していますが、この _T() マクロもマルチバイト用とワイドキャラクタ用との切り替えはコンパイルオプションの指定によるので、テンプレート引数の指定とは関係ありません
ですからこれもテンプレート引数の指定に応じてマルチバイト用とワイドキャラクタ用とを使い分けられるようにする必要があります
sprintf と OutputDebugString を外に追い出したのと同じように _T() マクロも外に追い出してみましょう
Fig.5
template<class T> inline const T* tstring_cast( const CHAR* s,const WCHAR* ){ return s; } template<> inline const WCHAR* tstring_cast<WCHAR>( const CHAR*, const WCHAR* ws ){ return ws; } template<class T> inline T tchar_cast( CHAR c, WCHAR ){ return c; } template<> inline WCHAR tchar_cast<WCHAR>( CHAR, WCHAR wc ){ return wc; }
template<class T> void debug_dump_vector( const std::vector<std::basic_string<T> >& v, int indent = 0 ){ int n = 0; for( std::vector<std::basic_string<T> >::const_iterator p = v.begin(); p != v.end(); ++p ){ T s[100]; snprintf<T>( s, elementsof( s ), tstring_cast<T>("%*c[%d] ",L"%*c[%d] "), indent, tchar_cast<T>(' ',L' '), n ); OutputDebugString<T>( s ); OutputDebugString<T>( *p ); OutputDebugString<T>( tstring_cast<T>("\n",L"\n")); ++n; } }
このようにすればマルチバイト用の文字列リテラルとワイドキャラクタ用の文字列リテラルをテンプレート引数の指定に応じて使い分けることができます
ただ、毎回 Fig.5 のように書くのでは面倒ですし間違いの元なので、下記のようなマクロを用意してもいいかもしれません
Fig.6
#define tstring_cast_( T, s ) tstring_cast<T>(s,L##s) #define tchar_cast_( T, c ) tchar_cast<T>(c,L##c)
template<class T> void debug_dump_vector( const std::vector<std::basic_string<T> >& v, int indent = 0 ){ int n = 0; for( std::vector<std::basic_string<T> >::const_iterator p = v.begin(); p != v.end(); ++p ){ T s[100]; snprintf<T>( s, elementsof( s ), tstring_cast_(T,"%*c[%d] "), indent, tchar_cast_(T,' '), n ); OutputDebugString<T>( s ); OutputDebugString<T>( *p ); OutputDebugString<T>( tstring_cast_(T,"\n"))); ++n; } }
ここまでテンプレートにして最後にマクロが出てくるのは残念ですが、ともかくこれでようやくコンパイルできるようになりました
関数は呼び出しは解決が実行時になってしまうので、コンパイル時に解決したいということであれば、あるいはこのようにしてもいいでしょう
template<typename char_type,char c,wchar_t> struct tchar_cast { static const char_type value = c; }; template<char c,wchar_t wc> struct tchar_cast<wchar_t,c,wc> { static const wchar_t value = wc; }; #define tchar_cast_(char_type,c) tchar_cast<char_type,c,L##c>::value
このようにテンプレートの中でマルチバイト文字とワイドキャラクタとを使い分けるには、それらをさらに別のテンプレートに追い出してあげることで対応できました
実はこのような仕組みは STL でも使われています
例えば std::char_traits<> は正にこのような目的のためにあるものです
std::char_traits<> はテンプレート引数に応じて strcpy や strlen, strcmp などと wcscpy, wcslen, wcscmp などとを呼び分けられるように用意されているものです
これは何もマルチバイト文字とワイドキャラクタに限った話ではありません
テンプレート引数に応じて何か処理を分けたいというときにはその場合分けを別のテンプレートに追い出すことで実現することができます
また別の例をみてみましょう
下記のように「テンプレート引数に指定された型と同じ型の signed/unsigned な型を明示して使いたい」というようなときにはどうしたらいいでしょうか
template<typename value_type> struct CFoo { union { value_type m_buffer[256]; struct { unsigned value_type m_length; value_type m_data[255]; }; }; . . .
このような宣言はできないので、ここにテンプレート引数 value_type に応じて適切な unsigned型になるような何らかの仕組みが必要です
unsigned value_type m_length;
テンプレート引数に応じた場合分けなのですから、ここもテンプレートの外に追い出して別のテンプレートにします
template<typename _plain_type, typename _signed_type = _plain_type,typename _unsigned_type = _plain_type> struct signed_unsigned { typedef _plain_type plain_type; typedef _signed_type signed_type; typedef _unsigned_type unsigned_type; }; // char だけは特別で char と signed char と unsigned char は文法上明確に区別されるので3つ template<> struct signed_unsigned<char> : public signed_unsigned<char,signed char,unsigned char> {}; template<> struct signed_unsigned<signed char> : public signed_unsigned<char,signed char,unsigned char> {}; template<> struct signed_unsigned<unsigned char> : public signed_unsigned<char,signed char,unsigned char> {}; // 他の(int と signed int など)は区別されないので2つだけ template<> struct signed_unsigned<short> : public signed_unsigned<short,signed short,unsigned short> {}; template<> struct signed_unsigned<unsigned short> : public signed_unsigned<short,signed short,unsigned short> {}; template<> struct signed_unsigned<int> : public signed_unsigned<int,signed int,unsigned int> {}; template<> struct signed_unsigned<unsigned int> : public signed_unsigned<int,signed int,unsigned int> {}; template<> struct signed_unsigned<long> : public signed_unsigned<long,signed long,unsigned long> {}; template<> struct signed_unsigned<unsigned long> : public signed_unsigned<long,signed long,unsigned long> {};
こうしておいて次のようにすることで value_type がどんな型であっても unsigned な型を指定することができるようになります
typename signed_unsigned<value_type>::unsigned_type m_length;
テンプレート引数に応じた場合分けをしたければそこをテンプレートの外に追い出して別のテンプレートとする
これを覚えておきましょう
余談
上の例で char だけ3つになっています
int や short などでは signed も unsigned 指定されていない単に int や short などと書いた型は signed が付いている型と同じ型としてみなされます
つまり int と signed int や short と signed short は常に全く同じ型です
signed も unsigned も付いていなければ signed が省略されているものとして扱われます
これに対して char は単に char と書いたときと signed char と signed を明示したものは全く別の型として扱われますので気をつけてください
実際文法上 char と signed char と unsigned char は全く別の型です
コンパイルオプションで char を暗黙的に unsigned として扱うように指定されているような場合も char と unsigned char は全く別の型です