C++マニアック

C++,STL,transform,toupper,tolower,文字列変換

このページでは STL のアルゴリズム transform について解説します。例として ASCII 文字の大文字/小文字に変換をしてみます。1 文字の変換の場合には toupper/tolower 関数を単純に使えばよいのですが、文字を変換する場合には、これを for 文で回すとか transform を使うなどのいろいろなやり方が考えられます。

  1. 最も簡単!
  2. for 文で回す
  3. transform を使ってみる
  4. 応用してみる
  5. 省略形の使用
  6. CString の MakeUpper() に勝つ ToUpper()

最も簡単! ページの先頭へ

何も考えずに、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。

for 文で回す ページの先頭へ

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++ マニアック的でないというか。

transform を使ってみる ページの先頭へ

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 文を回すよりもずっと効率の良いコーディングになるということを理解してください。

ナオ : これって、本当にだんだん効率がよくなっていくの? チョイあやしくない?
店主 : 何をおっしゃるウサギさん。信じるものは、救われる、のですぞ。

CString の MakeUpper() に勝つ ToUpper() ページの先頭へ

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;
}

ナオ : やっぱり、信じるものはだまされるって感じ?
店主 : 大事なのは、何を信じるかってことだよね。
ナオ : って、店長に言われてもねぇ。