Qt 5.5.0 で qDebug() << QString の出力がエスケープされるようになった件のまとめ
はじめに
2015年7月27日に Qt の開発者用のメーリングリストに「Backwards compatibiltiy break in Qt 5.5」という投稿がありました。
内容は、 Qt 5.5.0 で qDebug() << QString の 挙動が変わり、しかも 不便になった ので、ロシアの Qt コミュニティはとても困っていて、[QTBUG-47316] QDebug should not escape printable non-US-ASCII characters というバグレポを書いてやりとりをしていたけれど、担当者では話にならないから「もっと偉い人と話がしたい」という少々過激なものでした。
挙動の変更
Qt 5.4 までは
qDebug() << QString::fromUtf8("Qt めっちゃキュート");
を実行すると
"Qt めっちゃキュート"
とデバッグログが出力されていました。
Qt 5.5.0 では以下のようにUS- ASCII 以外の文字については Unicode のコードポイントが表示されるようになっています。
"Qt \u3081\u3063\u3061\u3083\u30AD\u30E5\u30FC\u30C8"
この変更は 2014/11/19 に Qt Core のメンテナによって作成された以下のパッチによるものです。
QDebug: pretty-print QStrings and QStringRefs [ChangeLog][QtCore][QDebug] Printing of QStrings and QStringRefs whenever "noquote" is not active now prints the strings in a format that can be copied back to C++ code. All characters that aren't printable in US-ASCII are escaped (this includes printable Unicode characters outside of US-ASCII). Pretty-printing will not respect QTextFormat padding or field widths.
Qt 5.4 で導入された QDebug::noquote() が指定されていない場合にはエスケープするという説明が書いてあります。
The Qt Company のプログラマがレビューをし、2015/01/12 に当時の dev ブランチ(=現在の 5.5 ブランチ)にマージされました。
この変更の意図
1つ目は「qDebug() << QString」は QString 自体をデバッグするためのものだ(から内容はより低レベルの方が望ましい)という主張です。
2つ目は「UTF8 で出力しても正常に表示されないプラットフォームがある」というものです。
メーリングリストでの議論の内容
議論0:偉い人出てこい
Thiago は Qt Core モジュールのメンテナのため、基本的にこれに関する彼の決定は覆せません。ただし、彼自身が設計した Qt Project のルールとして、「メンテナレベルで解決できない問題などの最終判断はチーフメンテナが行う」ことになっていて、Thiago としては「Lars 以外にこの変更を元に戻せる人はいないよ」という感じでした。
議論1:QString 自体のデバッグであるべきだ
Thiago は「pаypal.com と pаypаl.com と раураl.com のような同じように見えるけれど実は違う文字列を見分けるために使われるべきだ」、「今まで使い方を間違っていただけなので今後は正しい使い方をしてくれ」と主張していて、もし今までのように QString の中身をそのまま表示させたければ noquote() を使うか、qPrintable(実際には qUtf8Printable)を使うのが正しいとのことでした。
いままでずーっと qDebug() は文字列の内容を確認するために気軽にあちこちで使われてきたので、いまさら QString 自体のデバッグに使えというのは現実的には難しい。特に、既存のコードの該当部分すべてに qPrintable をつけるなんてむちゃくちゃだというのが反対派の主張です。
Lars の意見は「Qt 自体の開発中でさえ 90% は単に文字列の内容を見るために使ってるし、Qt を使ってなんかアプリを作ってる場合は 99% はそうだろう」というものでした。
議論2:なんでこんな変更がなんの議論もなしに入るんだ
こんな影響範囲の大きい変更がなんの議論も周知もなしに入るなんておかしいという意見もありました。
Thiago は、レビュープロセスも通したし、ChangeLog にも書いてあるし(ただし、Important Behavior Changes のタグは未設定)(さらに Qt 5.5 のリリース記事 はこの変更には言及しておらず、ChangeLog へのリンクもないけどこれはリリースチームとかマーケティングチームの問題だから)、修正自体は問題ないと反論しています。
議論3:後方非互換について
Thiago から「バグに依存しているコードにしてみたらすべてのバグフィックスは後方非互換だね」という名言が飛び出しました。影響範囲や Qt としてどうあるべきかを総合的に判断した上で今回の変更を行ったということです。
これに関して、Lars は「影響範囲については少なく見積もりすぎてる」「non-ascii(特に non-latin)な Qt ユーザーはすごいいっぱいいるぞ」と指摘しています。
議論4:UTF8 だと問題があるバックエンドについて
Thiago 曰く「Android、slog2(QNZ), syslog(Qt 5.6 からサポート予定), 標準出力に関してはロケールが UTF8 でない場合に mojibake になる。特にベトナム版以外の Windows は UTF8 ではない。逆に Mac はユーザーがあほなことをしていない限りは大丈夫」とのことでした。
Lars は「最近のAndroid と Linux 系はほとんどが UTF8 だし、Windows は OutputDebugString がデフォルトなので問題ない。QNX で特別な対応が必要であればそっちをやるのが筋じゃないか」と言っていました。
議論5:マニピュレーターを導入する?#ifdef?環境変数?
qDebug() << (no)escape << QString みたいにするのはどう?とかコンパイル時や実行時に選択できるようにしようという提案が色々なされました。
それ自体には大きな反論はありませんでしたが、これらの場合デフォルトをどっちにするかでやはりもめました。
議論6:QInfo がエスケープされるのはおかしい
これはバグレポの方ですが、「QInfo の変更は今回の変更の後に入ったものなのでしらねー」と Thiago は言っています。
議論7:既存の Qt のコードも直すの?
ロシア人が頑張っていたので 日本人も頑張ってみました。
- QDebug operator<<(QDebug debug, const QDir &dir)
- QDebug operator<<(QDebug, const QJsonValue &)
- QDebug operator<<(QDebug, const QVariant &)
などの中で QString が qDebug に渡されていてエスケープ表示されるのを qPrintable で囲うのかどうか聞いてみました。
また、QIODevice のように Qt で警告を出しているところも全部チェックして、必要があれば直さなければいけないよね?という質問と、一番面倒くさそうな QStringList とか QVariantMap とかコンテナのキーや値に QString が使われているケースをどうするのかも聞いてみました。
積極的に反対はしないけど、現実的に無理じゃない?というつもりで書いたメールで、Thiago からは回答はありませんでしたが、別の人に「This is another very good point is that Qt itself uses operator<< with filenames or potentially translated error messages a lot.」と言われました。
結論
技術的なことから現実的なことまで色々話し合った結果、今回の挙動の変更はやはりまずいだろうという意見が多数をしめました。それを踏まえて、Thiago から Lars に「これどれにするか決めて」と出されたリストが以下になります。
1) qDebug for QStrings should a) escape everything non-US-ASCII (5.5.0 behaviour) b) escape everything that isn't QChar::isPrint c) escape only US-ASCII control characters (0 to 31), backslash and quote d) escape only NUL, backslash and quote e) escape nothing (5.0 through 5.4 behaviour) [I haven't included option f) mangle irrevocably (4.x behaviour)] 2) ditto for qDebug for QByteArrays, except that for: b1) escape everything that isn't isprint() from ctype.h (locale dependent) b2) escape everything that isn't printable under Latin 1 Note that QByteArray::toUpper and toLower do not and have never used ctype.h and are instead fixed to Latin 1. 3) QtTest should a) follow qDebug and be consistent b) do it differently, in which case we need to select which of the options above 4) when to apply those changes, which are technically behaviour changes a) 5.5.1 b) 5.6.0
これを受けてチーフメンテナの決定が行われ、この件は一段落となりました。
Change how QDebug escapes QStrings in the output という挙動の変更を元に戻すパッチと、印字できない文字に対する仕様のドキュメント更新の Doc: update QDebug documentation to talk about the escaping が現在レビュー中で、Qt 5.5.1 に取り込まれる予定です。
まとめ
良かれと思ってやったことが裏目に出ることはありうることで、それがメンテナだったりすると色々ややこしいことになりますが、オープンソースのプロジェクトとしてちゃんと対応ができたのはとてもよかったと思います。
この件に興味のある方は「[Development] Backwards compatibiltiy break in Qt 5.5 のスレッドに目を通すと、もっと様々な事情や技術的な問題がわかると思います。