このページでは STL のアルゴリズム transform について解説します。例として ASCII 文字の大文字/小文字に変換をしてみます。1 文字の変換の場合には toupper/tolower 関数を単純に使えばよいのですが、文字列を変換する場合には、これを for 文で回すとか transform を使うなどのいろいろなやり方が考えられます。
何も考えずに、MFC の CString を使って、変換してしまうという方法が、最も簡単かもしれません。
#include <afx.h> int main() { CString cstr("aBcDe"); cstr.MakeUpper(); // 大文字に変換、安易 printf("%s\n", LPCTSTR(cstr)); return 0; }
これは、安直ですが、発展性はありません。
注: 上記は、そのままでは BorlandC++ 5.5 では、コンパイルできません。VC++6 では OK。
C 言語風には、for 文で回すのが常套手段といえます。
#include <stdio.h> #include <ctype.h> int main() { char acString[] = "aBcDe"; {for (char* pc = acString; *pc; pc++) { *pc = toupper(*pc); }} printf("%s\n", acString); return 0; }
まあ、悪くないですね。上記コードは、文字列が 0 で終わっている(いわゆる NULL 終端文字列)ということを仮定しています。学生なら、これで満点というところです。しかし、面白みはありませんね。C++ マニアック的でないというか。
C++ 的には、STL のアルゴリズム、transform を使ってみるという方法が考えられます。やってみましょう。
#include <string> #include <algorithm> #include <cctype> #include <cstdio> using namespace std; int main() { string str("aBcDe"); transform(str.begin(), str.end(), str.begin(), toupper); // とりあえず、1 行で書ける。 printf("%s\n", str.c_str()); return 0; }
上記は VC++6 ではコンパイルできましたが、BorlandC++5.5 では、コンパイルできませんでした。これは、上記で transform の引数の toupper の部分に期待される変数型は char(*)(char) ですが、<cctype> で定義されている toupper の型は int(*)(int) なので、BorlandC++5.5 では、型チェックが厳密で、通らないようです。VC++6 はその点、おおらかということのようです。
回避策としては、型の合うグローバル関数を定義してしまうという方法が、考えられます。これは、お勧めはしない方法です。
#include <string> #include <algorithm> #include <cctype> #include <cstdio> using namespace std; char ToUpper (char cX) { return toupper(cX); } // 型の合うグローバル関数を定義する。 int main() { string str("aBcDe"); transform(str.begin(), str.end(), str.begin(), ToUpper); // そして使う。 printf("%s\n", str.c_str()); return 0; }
上記の方法を、なぜ、お勧めしないかというと、グローバル関数 ToUpper は、その実体をどこかの *.cpp で定義しなくてはならないからで、ToUpper の定義をヘッダに入れてしまい、後はヘッダをインクルードするだけという使い方ができないからです。
この問題の回避策としては、ToUpper をテンプレート関数にしてしまうという方法が考えられます。つまり、次のようにします。
#include <string> #include <algorithm> #include <cctype> #include <cstdio> using namespace std; template <typename T_char> T_char ToUpper (T_char cX) { return toupper(cX); } // テンプレート関数として ToUpper を定義する。 int main() { string str("aBcDe"); transform(str.begin(), str.end(), str.begin(), ToUpper<char>); printf("%s\n", str.c_str()); return 0; }
実際に上記を試してみると、今度は BorlandC++5.5 では、問題なく動作しましたが、VC++6 では、理不尽なことに「error LNK2001: 外部シンボル ""char __cdecl ToUpper(char)" (?ToUpper@@YADD@Z)" は未解決です」というエラー(意味は、リンク時に ToUpper が見つからないということ)になってしまいました。
コンパイラは、管理人よりも 1000 倍以上頑固なので、人生を楽しく生きるために抵抗するのはやめて、解決策として、上記 ToUpper と同じ型の関数オブジェクトを定義してみます。この方法は、発展性があり、両コンパイラで問題なくコンパイルできます。
#include <string> #include <algorithm> #include <cctype> #include <cstdio> using namespace std; // 関数オブジェクトを定義する。本来は、ヘッダに入れる。 struct ToUpper { char operator()(char c) { return toupper(c); } }; int main() { string str("aBcDe"); transform(str.begin(), str.end(), str.begin(), ToUpper()); // () はコンストラクタを呼んでいる。 printf("%s\n", str.c_str()); return 0; }
関数オブジェクトが分からないという方は、()、関数呼び出し演算子、関数オブジェクトをご覧ください。
transform は文字列の変換だけでなく、他にも応用が可能です。例えば、vector に入っている数値をクリッピング(値の範囲制限)する例は、次のようになります。
#include <vector> #include <algorithm> #include <cstdio> using namespace std; // 値を制限する関数オブジェクトを定義する。本来はヘッダに入れる。 class CMinMaxLimitter { private: int m_iMin; int m_iMax; public: CMinMaxLimitter(int iMin, int iMax) : m_iMin(iMin), m_iMax(iMax) {} int operator()(int iValue) { if (iValue < m_iMin) { return m_iMin; } else if (iValue > m_iMax) { return m_iMax; } return iValue; } }; int main() { vector<int> veciBuffer(100); // このあたりで veciBuffer に値を設定する。詳細略。 // 10〜20 の範囲にクリップする。 transform(veciBuffer.begin(), veciBuffer.end(), veciBuffer.begin(), CMinMaxLimitter(10, 20)); return 0; }
ある値以上、ある値以下、というような場合には、上記の自作の CMinMaxLimitter ではなく、<algorithm>に用意されている max、min などを使えば簡単に変換できます。
transform は、便利ではありますが、begin() と end() を何度も書かなくてはならないので、何とかならないかということで、簡単記述のためのテンプレート関数を定義してみます。C++ マニアック的には、こうなります。
#include <string> #include <algorithm> #include <cctype> #include <cstdio> using namespace std; // 関数オブジェクトを定義する。本来は、ヘッダに入れる。 struct ToUpper { char operator()(char c) { return toupper(c); } }; // インプレイスで変換する場合の transform の省略形を定義する。これも本来、ヘッダに入れる。 template<typename T_array, typename T_converter> T_array::iterator transform_in_place(T_array& rarray, T_converter& rconverter) { return transform(rarray.begin(), rarray.end(), rarray.begin(), rconverter); } int main() { string str("aBcDe"); transform_in_place(str, ToUpper()); // 省略形を使って変換する。 printf("%s\n", str.c_str()); return 0; }
ToUpper と transform_in_place の定義をヘッダに入れてお膳立てしておけば、main() 関数の中の呼び出しは、すっきりとすることになります。上記の呼び出しは、str を大文字に、インプレイスで変換するという意味に見えますよね。少なくとも、上で紹介した、学生さん用のコードよりは、何をしようとしているか分かりやすい記述になっています。
上記を、ちょっと見ると、コードの量が増えて意味ないじゃないか、という感じを受けるかもしれませんが、ヘッダに入る部分は、使い回しが効く部品であり、このような部品のコーディングを進めていくと、次第にプログラミングが楽に、しかも高度になっていきます。結果的に、毎回 for 文を回すよりもずっと効率の良いコーディングになるということを理解してください。
ナオ : これって、本当にだんだん効率がよくなっていくの? チョイあやしくない?
店主 : 何をおっしゃるウサギさん。信じるものは、救われる、のですぞ。
transform の使い方ってことで、いろいろ書いてきましたが、文字列を大文字化するというときには、C++ マニアック的にはこうする、という例を挙げておきます。
#include <string> #include <locale> using namespace std; struct ToUpper : public string { ToUpper(const string& rstr) { {for (int iIndex = 0; iIndex < rstr.size(); iIndex++) { *this += toupper(rstr[iIndex], locale()); }} } }; int main() { string strCapital = ToUpper("Neel"); return 0; }
ナオ : やっぱり、信じるものはだまされるって感じ?
店主 : 大事なのは、何を信じるかってことだよね。
ナオ : って、店長に言われてもねぇ。