Tech Racho エンジニアの「?」を「!」に。
  • 開発

echoコマンドをC++に移植する

こんにちは。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++に移植するのであれば、このくらいがちょうどいい落とし所かもしれません。

関連記事

4.4BSD-Lite2のUNIXコマンドをC++に書き換える開発で学んだ事 Part1

[C++] イテレーターを作る【BPS Advent Calendar: 12/20】

CMakeをより便利にするライブラリ “CMakeSupports” がオープンソースになりました


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。