ベタなforよりbetterなfor
最近リファクタしたコードを紹介します。
unsigned int login_user_id_list[MAX_USER_ID_COUNT];
int i = 0;
for (i < MAX_USER_ID_COUNT; i++)
{
if (login_user_id_list[i] == INVALID_USER_ID)
{
continue;
}
if (login_user_id_list[i] == user_id)
{
break;
}
}
if (i == MAX_USER_ID_COUNT)
{
// ユーザーがログインしていないケース
}
else
{
// ユーザーがログインしているケース
}
コメントを付けたのは私です。本当はコメントもなかったので、初めて見た時に一瞬「...?」ってなって、理解するまでちょっと時間かかりました。
最終的に私が提案したコードは以下のようになっていました。
#include <algorithm>
const auto found = std::find(std::begin(login_user_id_list), std::end(login_user_id_list), user_id);
if (found == std::end(login_user_id_list) || *found == INVALID_USER_ID)
{
// ユーザーがログインしていないケース
}
else
{
// ユーザーがログインしているケース
}
std::find
の引数はイテレータになっていって、生配列の場合はstd::begin()
とstd::end()
で作成していますが、本当はbegin()
とend()
というメンバー関数が用意されてあるコンテナとセットで使うのがお勧めします。
例えば、std::list
を使った場合は、std::find
のコールが以下のようになります。
std::list<unsigned int> login_user_id_list(MAX_USER_ID_COUNT);
const auto found = std::find(login_user_id_list.begin()), login_user_id_list.end(), user_id);
別にfor
ループを使っても問題はないですが、std::find
のような特化した関数がある場合は、そちらを使った方がコードが分かりやすくなると思います。
for
ループは万能で何にでも使えるからこそ、実際に何をやっているかを理解するまで全部一通り読まないといけないため、時間がかかります。
でもコードを書いている時は、まずfor
ループを書きたくなる気持ちはよく分かります。私も大体for
ループから始めると思います。プログラマーはみんなfor
ループに慣れているので、こういうコードは追いやすいかもしれないですが、場合によって分かりづらかったりします。
それに、for
ループはインデックスを扱ったりするため、ミスの余地もそれになりにあります。まずはfor
ループを書いて、これはいけるなって分かったら、次のステップで、コードをもうちょっと分かりやすくしてみるという順番でもいいと思います。
std
ライブラリは<algorithm>
というヘッダーによくあるfor
ループのパターンの代わりに使える関数をたくさん用意しています。
std::find
の他にも
-
std::for_each
- 全ての要素に対して処理を行う
-
std::all_of
- 全ての要素が条件を満たしているかを調べる
-
std::any_of
- どれかの要素が条件を満たしているかを調べる
-
std::none_of
- 全ての要素が条件を満たしていないかを調べる
-
std::count
- 指定された値である要素の数を数える
-
std::count_if
- 条件を満たしている要素の数を数える
-
std::replace
- 指定された値と一致する要素を指定された値に置き換える
-
std::fill
- 指定された値で出力の範囲に書き込む
などあります。
全部はここで紹介できないですが、std::for_each
を代表として紹介しておきます。
std::for_each
はfor
ループをイテレータと関数で再現している関数です。std::for_each
のためにわざわざ関数を定義するのは手間なので、大体ラムダ式関数とセットで使います。
一番最初のリファクタしたfor
ループをstd::for_each
で再現しようとすると、for
とstd::for_each
の違いが分かりやすくなります。
std::for_each(std::begin(login_user_id_list), std::end(login_user_id_list), [&](unsigned int current_id) {
if (current_id == -1)
{
return;
}
if (current_id == user_id)
{
i = current_id;
}
});
if (i == MAX_USER_ID_COUNT)
{
// ユーザーがログインしていないケース
}
else
{
// ユーザーがログインしているケース
}
for
ループのcontinue
はstd::for_each
ではreturn
になりますが、for
ループのbreak
はstd::for_each
では再現できないです。
std::for_each
は実際に各要素で同じことを行いたい場合に使うため、早くループからリターンすることはできないです。そのため、このstd::for_each
の使い方では、user_id
を見つけたとしても、ループし続けます。
やはりこのfor
ループ場合はstd::find_if
が一番適切だと思います。
for
ループとstd::for_each
のどちらが読みやすいかってなると好みの問題になりますが、<algorithm>
にある関数の大半はラムダ式関数とセットで使うため、こういう書き方になると思います。
よく書いているfor
ループがあったら、<algorithm>
にそのユースケースに特化した関数がないかを見てみるといいと思います。
Discussion
正直この粒度であれば何でもいいと思いますが、可読性はforループの方が高いと思います。
C++は記述が長ったらしくなることが多く、読みにくい場合が多いので。
後は速度面の比較もあった方がいいと思いますよ。
コード違いますが、簡単に比較したものを載せておきます。
最後に、既存のコードをリファクタリングをする場合は不具合混入の可能性もあるので、メリットとデメリットをよく考えて行った方がいいと思います。開発中など品質を取る前ならいいのですが…。
コメントありがとうございます。パフォーマンスの観点は確かに大事だと思います。今後の参考にいたします。ありがとうございました。