[C++]std::coutやstd::stringをprintf風にフォーマットする。

 私はstreamが嫌いである。scanf、printf系の関数を使うほうがよほど分かりやすく間違いも生じにくいと勝手に思っている。特にiomanipを使ったフォーマットは最悪だ。あんな記述コストが大きくて分かりにくい仕組みをどこの阿呆が考えやがったのか。
 C++20からは標準ライブラリにformatが加わるものの、これはprintf系とは振る舞いが異なる。そもそも私は開発中のライブラリでC++14以上をサポートするようにしているので、C++20の機能を導入することはできないし、MSVC2019時点ではそもそもC++20は部分的にしかサポートされておらず、その中にformatは含まれていない。これの実装は既にGitHubでfmtという名前で公開されており広く使われているのだが、外部公開するためのライブラリ開発を行っている私にとって余計な依存関係を増やすのは好ましくない。

 とはいえそれでも時と場合によってはstreamを使わざるを得ない状況というのも生じる。例えば私が作っているとあるライブラリは、標準出力をリダイレクトして別のところに書き出したいという要望があり、rdbuf関数で出力先を書き換えられるように標準出力は全てcout、cerrが使われている。

 streamのフォーマットは不便極まりないので、可能であればprintf-likeなフォーマット関数があるとよいのだが、これがなかなか悩ましい。

不完全な方法。

 例えば次のように、sprintfやsnprintfを使って整形されたstd::stringを出力させる方法はあるが、これは不完全だ。文字数制限が生じてしまう。バッファを長めに確保しておけばよっぽど問題にはならないが、気持ちが悪い。

template <class ...Args>
std::string Format(const std::string& fmt, Args ...args)
{
    char buf[1024];//1024文字を超えるとバッファオーバーフロー。snprintfであっても文字数制限は生じる。
    sprintf(buf, fmp.c_str(), args...);
    return std::string(buf);
}

気持ち悪いが完全な方法。

 snprintf関数はその戻り値として格納された文字数を返す。なので文字数の判定のために一度、実際の文字列の格納のためにもう一度、都合二度snprintf関数を呼び出すことで、上の文字数制限を突破することはできる。ただ、これのパフォーマンスがどうのこうのよりも、この実装そのものが気持ち悪い。snprintfを二度呼び出すのがもう蕁麻疹が出そうなほどに気持ち悪いのだ。

template <class ...Args>
std::string Format(const std::string& fmt, Args ...args)
{
    std::size_t len = snprintf(nullptr, 0, fmt.c_str(), args ...);
    std::vector<char> buf(len + 1);
    snprintf(buf.data(), len + 1, fmt.c_str(), args ...);
    return std::string(buf.data());
}

全部自作する方法。シングルヘッダーライブラリ。

 さすがに腹立たしいので、私は色々と悩んだ末、printfと近い動作をするFormat関数を作成しシングルヘッダー化して使っている。ソースコードはこちら。私がGitHubで公開しているGnuplot用ライブラリに同梱されているので、そちらへのリンクだ。
 これはostringstreamによってprintfのような振る舞いを再現したものである。以下のように使うことができる。

std::string str = adapt::Format("%03d %5.3lf %s", 12, 3.4, "5678");
//std::cout << adapt::Format("%03d %5.3lf %s", 12, 3.4, "5678");戻り値はstringなので、coutに直接出力してもよい。

 ある程度printfと同様の動作をするが、部分的に未実装の機能があったり、仕様の問題なのか厳密に再現できないところもあった。ただ折角自作するのならと、少しばかりprintfにない機能を追加したりもしている(%sがstd::stringに対応していたり、bool値をtrue/falseで表示できたり、万能の長さ修飾子"?"を使えたり)。代用品としてはそこそこ使えるとは思う。実行速度?知らん。

[C++]QFileDialogで最後に開いたフォルダ。

 QFileDialogは最後に開いたフォルダを保存しておき、次にダイアログを開いた時は最初からそのフォルダを開くようになっている1。この“最後に開いたフォルダ”は全てのQFileDialogのインスタンスで共有されているので、その情報はstaticメンバないしグローバル変数として保管されているはずだ。この“最後のフォルダ”を取得する方法はないのだろうか。例えばこのフォルダ情報を.iniに書き出しておいて、アプリケーションの次回起動時に“最後のフォルダ”を復元できれば地味にありがたい。

 結論から言えば、スマートな方法はない。少なくともQFileDialog::lastDir()のような便利な静的メンバ関数などは存在しない。
 QFileDialogの実装を見ていると、確かにこの情報を格納している"lastVisitedDir"というグローバル変数が存在するのだが、これは完全に.cppファイルの中に隠蔽されており、QFileDialogのpublicメンバからも取り出すことが出来ないようだった。

 代替案はある。QFileDialogを使うたびに、取得したファイルの情報からフォルダを取り出して、自ら用意した変数に格納しておけばよい。例えば次のような関数を用意しておき、QFileDialog::getOpenFileNameの代わりに使う。そうすることで、gLastDirがGetOpenFileNameを呼ぶたびに更新される。

QUrl gLastDir;

QString GetOpenFileName(QWidget *parent = nullptr,
                               const QString &caption = QString(),
                               const QString &filter = QString(),
                               QString *selectedFilter = nullptr,
                               QFileDialog::Options options = QFileDialog::Options())
{
    const QStringList supportedSchemes = QStringList(QStringLiteral("file"));
    QFileDialog dialog(parent, caption, gLastDir.toLocalFile(), filter);
    dialog.setSupportedSchemes(supportedSchemes);
    dialog.setOptions(options);
    if (selectedFilter && !selectedFilter->isEmpty())
        dialog.selectNameFilter(*selectedFilter);
    if (dialog.exec() == QDialog::Accepted)
    {
        if (selectedFilter)
            *selectedFilter = dialog.selectedNameFilter();
        QUrl path = dialog.selectedUrls().value(0);
        *gLastDir = dialog.directoryUrl();
        return path.toLocalFile();
    }
    return QUrl().toLocalFile();
}

 下策ではあるものの、一応動く。私の意図した通りの挙動ではあるし、開くダイアログによって“最後のフォルダ”情報の格納場所を分けたい場合などはむしろこれが有効だろう。とはいえ、たった一つQFileDialog::lastDir関数を用意してくれれば解決したのにと思わなくはない。


  1. 開くフォルダを明示的に与えなかった場合。

Linuxに不慣れな私が頑張ってシェルスクリプトで並列実行しようとした時の備忘録。

 下記のコマンドは動作確認してはいないので、万が一この記事を参考にしようとしているのなら十分に注意されたい。

目的

 ディレクトリ"text"内に、"test1.txt"、"test2.txt"、...、"testN.txt"というファイルがある。Nは連番になっているわけではなく、飛び飛びであるか、場合によっては数字ではないかもしれない。このファイルをあるプログラム"test_process"に渡して実行したい。

text_process ../test/test1.txt hogehoge

 ただしtest_processはとても時間のかかる処理なので、並列に実行したい。かつtestN.txtはスレッド数よりもずっと多いので、同時実行数は常にCPUコア数までとし、余剰分はコアに空きが出るまで待機させる。スレッドプールみたいなイメージである。

方法

 lsとxargsの2つのコマンドを組み合わせる。
 lsは知らない人のいないコマンドなので詳しい説明は省略するとして、testディレクトリ中のtestN.txtの一覧を取得するには次のようにすればよい。

ls test/test*.txt -1

 -1は見つかったファイルを1行ずつ改行しながら出力する。今回は特に必要はないと思うが、一応付けた。xargsで-Lオプションないし-nオプションを使う場合は改行以外の空白文字の取り扱いが変わってくるようなので、これの有無が影響するのかもしれない。
 findコマンドでも出来るのだろうが、違いはよく知らない。

 この出力結果をxargsへと渡す。

ls test/test*.txt -1 | xargs -P 6 -IAAA test_process AAA hogehoge

 このとき-Pオプションが同時実行数の指定。ここでは6個までtest_processを同時実行する。-Iオプションはlsコマンドの出力を1つずつ"AAA"という名前で受け取るという意味に解釈される。よってこの引数"AAA"をそのままtest_processの引数にしてやればよい。こうすることで、

test_process test/test1.txt hogehoge
test_process test/test2.txt hogehoge
...
test_process test/testN.txt hogehoge

というような感じで、test_processが最大6個まで同時に実行されていく。

その他

 今回、test_processの引数が"AAA hogehoge"という並びであったため、-Iオプションを用いた。もしhogehogeという追加の引数がなければ、あるいは"hogehoge AAA"という並びであったのなら、-Lまたは-nオプションでも可能だった。
 xargsがパイプから受け取った引数は、例えば

ls test/test*.txt -1 | xargs test_process hogehoge

のように-I、-L、-nいずれのオプションも与えずに実行した場合、

test_process hogehoge test/test1.txt test/test2.txt ... test/testN.txt

と、全てが一度にtest_processへと渡されてしまう。この引数の展開は必ずhogehogeの後に続き、hoehogeよりも前に配置することは出来ないようだ。
 引数が丸ごと展開されないようにするには、上述のように-Iオプションを使う方法もあるが、その他に-Lまたは-nオプションを使うこともできる。

ls test/test*.txt -1 | xargs -P 6 -L(or -n) 1 test_process hogehoge

こうすることで、

test_process hogehoge test/test1.txt
test_process hogehoge test/test2.txt
...
test_process hogehoge test/testN.txt

と1つずつ渡されるようになる。-L、-nの後に続く数字は、lsコマンドからの引数をいくつずつ展開するかを指示している。今回は1を与えたのでtest/testN.txtは一つずつtest_processに与えられた。  -Lオプションはlsコマンドの出力に対して、改行のみを区切りとみなし、それ以外の空白文字は区切り文字として扱わない。なのでlsコマンドに-1を与えておかないと、全ての出力結果が区切られることなくtest_processに渡されてしまう、と思われる。
 -nオプションは空白文字全般を区切り文字とみなすので、-1がなくても動くはず。
この微々たる違いにどのような意義があるのかはよく分からないが、例えばtest_processに与える引数の数が固定ではなく毎回異なっている場合、-Lオプションの"改行以外を区切りとして扱わない"性質が役立つ気はする。