こんにちは。yoshiです。TechRachoに書くのは久しぶりになってしまいました。今年はもっと書いていきたいと思っています。
先日、ikaさんの記事でechoコマンドを移植する話がありました。
C++に詳しい方の中には、「これはC++とは言えないのではないか?」と感じた方もいるのではないでしょうか。
確かに、「C++に移植する」という言葉から受けとるイメージとは異なっているかもしれません。
私自身、一応それなりの年数C++を使ってきた経験があるので、「C++らしいechoコマンド」を考えてみようと思います。
ついでに、せっかくなので、この記事では最新のC++17の機能も使ってみましょう。
C++版echo
私なりにC++らしく書いて見たechoコマンドが以下のようになります。
#include <iostream>
#include <string_view>
#include <vector>
#include <algorithm>
int main(int argc, char **argv) {
const std::vector<std::string_view> args(argv + 1, argv + argc);
const bool nflag = !args.empty() && args[0] == "-n";
std::for_each(
args.begin() + (nflag? 1 : 0),
args.end(),
[is_first = true](const auto &value) mutable {
if (is_first) {
is_first = false;
} else {
std::cout << ' ';
}
std::cout << value;
}
);
if (!nflag) {
std::cout << std::endl;
}
}
実行結果→[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ
std::string_view
C++17で新たに標準ライブラリに入ったクラスです。
以前から存在するstd::string
がメモリを確保するのに対して、std::string_view
は既存の文字列に対するビューを提供します。
C形式のchar
ポインタからビューを作成することもできるので、今回はargvが示す引数をstd::string_view
の動的配列(std::vector<std::string_view>
)にしています。
コピーのコストを省けるのでうまく使えばstd::string
とくらべて高速化に寄与しますが、メモリの所有権を持たないので寿命管理に注意が必要になります。
std::for_each
std::for_each
は、範囲の先頭と末尾をイテレーターで受取り、関数または関数オブジェクトを要素一個ごとに呼び出す標準ライブラリ関数です。
std::for_each
に渡す関数オブジェクトにはラムダ式を使ってみました。C++14で追加された初期化式付きコピーキャプチャとmutable
キーワードを使って、空白を出力する必要があるかどうかのフラグをラムダ式の中に閉じ込めています。
本当はせっかくなのでrange-based-forを使いたかったのですが、今回はループする範囲が条件によって変わってしまうこともあり、std::for_each
を使うことにしました。
実用性に関する問題点
実を言うと、上のコードは元のコードと比べると欠陥があります。
<iostream>
を使ったC++の標準入出力は、C標準ライブラリ関数による標準入出力よりも遅いのです。
どうして遅いのかについては機会があれば改めて書きたいところですが、標準ライブラリの設計段階のミスと言ってしまってもいいくらいではないかと私は思っています。
歴史的経緯というものがありますのでミスは言い過ぎかもしれませんが、現代のC++にとって、負の遺産になってしまっているのは否めません。
そんなわけで、パフォーマンスを気にするのであれば、ikaさんのコードのようにC標準ライブラリ関数を利用する方向が好ましく、それ故に、必ずしも「C++らしく書く」ことが正解とは言い切れません。
ikaさんのコードは以下のようなものでした。
#include <cstdio> #include <cstdlib> #include <cstring> int main(int argc, char *argv[]) { int nflag; /* This utility may NOT do getopt(3) option parsing. */ if (*++argv && !strcmp(*argv, "-n")) { ++argv; nflag = 1; } else nflag = 0; while (*argv) { fputs(*argv, stdout); if (*++argv) putchar(' '); } if (!nflag) putchar('\n'); exit(0); }
個人的には、屁理屈のようですが、「C++のコンパイラでコンパイルできるなら、とりあえずC++と言えるのではないか」と思っています。
ただし、このコードにはいくらか、C++の規格では実装依存となっている箇所がありますし、実装依存ではないですが、もう少し別の方法を使ったほうが良い箇所もあります。
std
名前空間の関数を使っていない
strcmp, putchar, fputs, exit
などのC関数は、C++でも利用することは可能です。互換性のためにCの標準ライブラリ(<stdlib.h>
など)も提供されているからです。
ただし、C標準ライブラリヘッダーの利用はC++では非推奨です。そのため、それらのヘッダーを利用したい場合、ヘッダーの拡張子を外し先頭にcを付けたもの(<cstdlib>
など)を利用するべきです。
ここまではいいのですが、そうして読み込まれたC関数はstd
名前空間に入ることになります。
例えば、<cstring>
をインクルードした時に、グローバル空間にstrcmp
関数が存在するかどうかは実装依存なので、C++コンパイラを使う以上はstd::strcmp
を利用するべきです。
exit(0);
はいらない
C++では、スコープを抜ける時にローカル変数のデストラクタがあれば呼ばれる仕様になっています。
しかし、exit
関数は呼び出された時点でプログラムを終了させてしまうので、main
関数の中で呼び出されるとmain
関数内のローカル変数の破棄が行われないことになります。
このコードではたまたま、デストラクタで何かを解放する処理を行うクラスは使われていませんが、C++でプログラミングをするのであれば、常にデストラクタ呼び出しがあることを意識しておいた方がいいでしょう。
もっとも、デストラクタが行っている処理がメモリ解放程度であれば、高速化のためにわざとデストラクタを起動しないようにするという戦略もないわけではありません。しかし、それは特殊な場合であり、普通はexit
を明示的に呼び出すのは避けるべきです。
また、C++の規格では、main
関数の戻り値を渡して、最終的にstd::exit
が呼び出される、と決まっています。
そのため、わざわざexit(0);
を呼び出さず、0を返せば正常終了したことになります。
また、main
関数に限って、関数末尾のreturn
文を省略すると0が帰ることが決まっているので、return 0;
すら書かなくても良いでしょう。
フラグにはbool
型を使う
Cでフラグにint
型を使うのは、真偽値を表すための型がなかったからです。C++にはbool
型が存在するので、それを使いましょう。
もっとも、C言語にも最近(と言ってもC99なのでだいぶ前から)は_Bool
型という真偽値型が導入されています。
そういった点を踏まえて最低限の修正を加えるとこうなります。
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main(int, char *argv[]) {
bool nflag;
/* This utility may NOT do getopt(3) option parsing. */
if (*++argv && !std::strcmp(*argv, "-n")) {
++argv;
nflag = true;
}
else
nflag = false;
while (*argv) {
std::fputs(*argv, stdout);
if (*++argv)
std::putchar(' ');
}
if (!nflag)
std::putchar('\n');
}
パフォーマンスを保ったままC++に移植するのであれば、このくらいがちょうどいい落とし所かもしれません。