C++ で 汎用的な isNullOrEmpty を作ってみた(本当に汎用的か疑問)
はじめに
デバイスクラスに限らず,ポインタ型に対してなんらかのメソッドを実行するとき,
Null チェックを行った後,メソッドを実行すると言うことは良くあると思います.
それらを汎用的にするとどうなるかメタプログラミングの勉強しながら作ってみたところ,
凄く複雑になってしまったという話.でも,お蔵入りして消してしまうのも何なので投稿してみました.
きっかけ
何らかのデバイスクラスのオブジェクトが接続済みか調べたいとき,以下の2つコード(A, B) では動作が異なります.
// Device *d; をどこかで宣言
// 以下のコードで d が接続済みか調べる.
d = NULL; // A と B どちらがエラーになる?
if (d->isOpened() || d == NULL) return false; // A
if (d == NULL || d->isOpened()) return false; // B
実行してみれば分かりますが,A はエラーが発生します.
これは,次のように解釈されるためでして,C++ に限らず,Python やJava,C# で同様の問題が起こると思います.
// 以下の構文は
if (条件文A || 条件文B) {
式
}
// 次の構文とほぼ等価
// スコープなどの関係で本当にどこまで同じか分かりません
if (条件A) {
式
} else if (条件B) {
式
}
条件A が真になると条件Bは評価されないため,上記コードの B はエラーが起こらず,A はエラーが発生してしまう.
そんな話をしていると,.Net では,.isNullOrEmpty という関数があるねーという話になりまして,
C++ で汎用的なコードにするとどうなるのか試してみました.
C++ での実装(関数で)
おそらく,テンプレートメタプログラムしないとまともにできないと考え,テンプレートメタの勉強がてら,
あれこれ,思考試行錯誤しながら作ってみました.すると以下の凶悪なコードができあがってしまいました(コメントは,後日追記).
コンセプトは以下の通り,
- isNullOrEmpty(value) で Null チェックと T::empty() を実行する.テンプレートであるため,型は何でも良い.全く同じ構文で std::string, std::vector が同じように評価される.
- 第2引数に関数およびメンバポインタを指定できる.例えば,isNullOrEmpty(value, &isIntegerString) という構文を許可する(もうここまで行くと,isNullOrReturnTrue みたいな感じになる?).
実際にテンプレートメタを使うため,部分特殊化を使わざるを得なかったのですが,面白く勉強にはなりました.ここまでしてまでこの機能が要るかと言われれば要らないですね ^_^;
.さらに言うなら,もっと短く簡潔にできそうです.
もう,ここまでするならラッパークラス作った方が早いですたぶん.
以下コード
// 作ったのが Visual Stdio 2008 なので,いろいろと足りない部分はご勘弁を.
// とくに,typename の入れ忘れが多いです.
# include <functional>
# include <deque>
# include <iostream>
# include <iomanip>
# include <memory>
# include <list>
# include <string>
# include <typeinfo.h>
# include <vector>
# include <boost/type_traits.hpp>
# include <boost/call_traits.hpp>
# include <boost/type_traits/is_pointer.hpp>
# include <boost/mpl/bool.hpp>
# include <boost/smart_ptr.hpp>
template <typename T, class FuncPtr, bool IsPointer, bool IsMemberFunction>
struct IsNullOrEmptyImpl;
template <typename T, class FuncPtr>
struct IsNullOrEmptyImpl<T, FuncPtr, true, true>
{
static bool get(typename boost::call_traits<T>::param_type value, FuncPtr funcPtr)
{
if (value == NULL) false;
return std::mem_fun(funcPtr)(value);
}
};
template <typename T, class FuncPtr>
struct IsNullOrEmptyImpl<T, FuncPtr, true, false>
{
static bool get(typename boost::call_traits<T>::param_type value, FuncPtr funcPtr)
{
if (value == NULL) false;
return funcPtr(value);
}
};
template <typename T, class FuncPtr>
struct IsNullOrEmptyImpl<T, FuncPtr, false, false>
{
static bool get(typename boost::call_traits<T>::param_type value, FuncPtr funcPtr)
{
return funcPtr(value);
}
};
template <typename T, class FuncPtr>
struct IsNullOrEmptyImpl<T, FuncPtr, false, true>
{
static bool get(typename boost::call_traits<T>::param_type value, FuncPtr funcPtr)
{
return std::mem_fun_ref(funcPtr)(value);
}
};
template <typename T, class FuncPtr>
bool isNullOrEmpty(const T &value, FuncPtr func)
{
return IsNullOrEmptyImpl<T, FuncPtr, boost::is_pointer<T>::value,
boost::is_member_function_pointer<FuncPtr>::value>::get(value,
func);
}
template <typename T>
bool isNullOrEmpty(const T &value)
{
typedef boost::remove_pointer<T>::type RemovePointerT;
return IsNullOrEmptyImpl<T,
bool (RemovePointerT::*)() const,
boost::is_pointer<T>::value,
true>::get(value, &RemovePointerT::empty);
}
template <typename T, class FuncPtr>
bool isNullOrEmpty(const boost::scoped_ptr<T> &value, FuncPtr func)
{
return isNullOrEmpty<const T *, FuncPtr>(value.get(), func);
}
template <typename T>
bool isNullOrEmpty(const boost::scoped_ptr<T> &value)
{
return isNullOrEmpty<const T *>(value.get(), std::mem_fun(&T::empty));
}
template <typename T, class FuncPtr>
bool isNullOrEmpty(const boost::shared_ptr<T> &value, FuncPtr func)
{
return isNullOrEmpty<const T *, FuncPtr>(value.get(), func);
}
template <typename T>
bool isNullOrEmpty(const boost::shared_ptr<T> &value)
{
return isNullOrEmpty<const T *>(value.get(), std::mem_fun(&T::empty));
}
bool isEmpty(const std::wstring *s)
{
return s->empty();
}
bool isEmpty(const std::wstring &s)
{
return s.empty();
}
int main()
{
std::wstring s = L"";
boost::scoped_ptr<std::wstring> scp(new std::wstring(L""));
boost::shared_ptr<std::wstring> shp(new std::wstring(L"a"));
bool (*isEmptyWString)(const std::wstring &) = &isEmpty;
bool (*isEmptyWStringPtr)(const std::wstring *) = &isEmpty;
std::wcout << std::boolalpha << isNullOrEmpty(s) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(s, isEmptyWString) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(&s) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(scp) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(scp, isEmptyWStringPtr) << std::endl;
boost::shared_ptr<std::vector<std::string> > v(new std::vector<std::string>());
std::wcout << std::boolalpha << isNullOrEmpty(v) << std::endl;
return 0;
}
上記の関数に至った経緯
なぜこうなってしまったかというと,汎用的に目指してしまったためです.
単純な関数
当然,単純な関数だったらもっと簡潔に書けます.wstring のみに特化すれば次のように簡単な関数で済みます.
bool isNullOrEmpty(const std::wstring *s)
{
if (s == NULL) return;
return s->empty();
}
int main(void)
{
std::wstring s = L"";
std::wstring *p = NULL;
std::wcout << std::boolalpha << isNullOrEmpty(p) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(&s) << std::endl;
return 0;
}
確かに単純で作りやすいですが,汎用性に欠けます.string すら受け付けません.
テンプレート化
上記の単純な関数から,vector* や list* など STL に対応したいときは次のようにテンプレート化すれば汎用的になります.これでも結構単純です.
template <class T>
bool isNullOrEmpty(const T *value)
{
if (value == NULL) return false;
return value->empty();
}
int main(void)
{
std::vector<int> *vp = NULL
std::wstring *p = NULL;
std::wcout << std::boolalpha << isNullOrEmpty(vp) << std::endl;
std::wcout << std::boolalpha << isNullOrEmpty(s) << std::endl;
return 0;
}
もう少し汎用的にしたいです.たとえば,SDK や 自作クラスで,empty() と等価なメソッドが Empty() だったり,isEmpty() だったり,IsEmpty() だったりします.これらにも対応したいときは,
tempate <typename T, class Func>
bool isNullOrEmpty(const T *value, Func func)
{
if (value == NULL) return false;
return func(value);
}
template <typename T>
bool isNullOrEmpty(const T *value)
{
return isNullOrEmpty(value, std::mem_fun(&T::empty))
}
と多重定義しておけば,
int main()
{
std::wstring s;
std::wcout << std::boolalpha << isNullOrEmpty(&s) << std::endl;
String s; // isEmpty() を持つ String クラス
std::wcout << std::boolalpha << isNullOrEmpty(&s, std::mem_fun(&String::isEmpty)) << std::endl;
}
第2引数がないときは,empty() が,第2引数にemptyメソッドを指定すれば指定したメソッドが実行されます.
しかし,これでは参照や値型に対応はできません,
int value = 10;
int &ref = value;
isNullOrEmpty(value); // コンパイル時エラー
isNullOrEmpty(ref); // コンパイル時エラー
これらに対応するためには,
template <typename T, class Func>
bool isNullOrEmpty(const T &ref, Func func)
{
if (&ref == NULL) return false;
return func(ref);
}
template <typename T>
bool isNullOrEmpty(const T &ref, Func func)
{
return isNullOrEmpty(ref, std::mem_fun_ref(&T::empty));
}
とすればいいのですが,これを定義したとたん
std::wstring s;
std::wstring &ref = s;
std::wstring *p = &s;
isNullOrEmpty(s);
isNullOrEmpty(ref);
isNullOrEmpty(p);
上記全て,bool isNullOrEmpty(const T &ref, Func func)
に行ってしまいます.
部分特殊化
というわけで,ポインタ型と値型を一緒くたにしたテンプレート関数を作るには,以下のとき,T 型 がポインタ型か値型か知る必要があります.
template <typename T>
void f(const T &ref)
{
std::wcout << L"0x" << &T << std::endl;
std::wcout << L"0x" << T << std::endl;
}
もうそうなると,std::is_pointer を使うしかありません.ここでテンプレートメタが登場してきます.
# include <type_traits> // C++11
# include <boost/type_traits.hpp> // boost(C++08 以前)
template <typename T>
void f(const T &ref)
{
std::wcout << std::boolalpha << boost::is_pointer<T>::value << std::endl;
}
int main()
{
std::wstring s;
std::wstring &ref = s;
std::wstring *p = &s;
f(s);
f(ref);
f(p);
}
とすれば,おそらく,
false
false
true
と返ってくるはずです.ここまでこれば後は部分特殊化を行えば振り分けができます.
# include <functional> // std::mem_fun, std::mem_fun_ref
# include <string> // std::string, std::wstring
# include <iostream> // std::wcout
# include <vector> // std::vector
# include <boost/call_traits.hpp> //
# include <boost/type_traits.hpp> //
///////////////////////////////////////////////////////////////////////////////////////////////////
//
//
// IsNullOrEmptyImpl Class
// IsNullOrEmptyImpl<T, Func>::get(value, funcPtr) で
// NULL チェックを行いつつ funcPtr の評価を行う.
// funcPtr は,返し値 bool であること.
//
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
* isNullOrEmptyImpl の定義
*/
template <class T, class Func, bool isPointer>
struct isNullOrEmptyImpl;
/**
* T がポインタの時
*/
template <class T, class Func>
struct isNullOrEmptyImpl<T, Func, true>
{
static bool get(typename boost::call_traits<T>::param_type value, Func func)
{
if (value == NULL) false;
return std::mem_fun(func)(value);
}
};
/**
* T が値または参照の時
*/
template <class T, class Func>
struct isNullOrEmptyImpl<T, Func, false>
{
static bool get(typename boost::call_traits<T>::param_type value, Func func)
{
return std::mem_fun_ref(func)(value);
}
};
/*******************************************************************************************/// ***
/**
* IsNullOrEmptyImpl のヘルパ関数
*
* @param value value.empty() を持つクラス
*
*************************************************************************************************/
template <typename T>
bool isNullOrEmpty(const T &value)
{
// ここで,T はポインタかもしれないので,以下でポインタ型を取り除いた型を得る
typedef boost::remove_pointer<T>::type RemovePointerT; // T* -> T
typedef bool (RemovePointerT::*FuncPtr)(void) const; // T のメンバポインタ型
return isNullOrEmptyImpl<T,
FuncPtr,
boost::is_pointer<T>::value>::get(value, &RemovePointerT::empty);
}
int main()
{
std::string s;
std::string &ref = s;
std::string *p = &s;
std::wcout << std::boolalpha << isNullOrEmpty(s) << std::endl;
std::wcout << isNullOrEmpty(ref) << std::endl;
std::wcout << isNullOrEmpty(p) << std::endl;
s = "a";
std::wcout << std::boolalpha << isNullOrEmpty(s) << std::endl;
std::wcout << isNullOrEmpty(ref) << std::endl;
std::wcout << isNullOrEmpty(p) << std::endl;
std::vector<int> v;
std::wcout << isNullOrEmpty(v) << std::endl;
std::wcout << isNullOrEmpty(&v) << std::endl;
return 0;
}
上記コードでは,T の empty メソッドにしか対応しておらず,empty 以外のメソッドや関数には対応してません.しかし,この当たりから,頭がこんがらがってきます.