Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MYDEVMODEの等価比較演算子の隠れバグを修正する #1079

Conversation

berryzplus
Copy link
Contributor

PR の目的

MYDEVMODEの等価比較演算子の実装に潜在バグを見付けたので修正します。

カテゴリ

  • 不具合修正

PR の背景

他のクラスのテストを書くために、
等価比較演算子をコピペ実装していく作業の中で気付きました。

具体的に何がマズいのか説明しづらいので、少し変わったやり方で進めたいです。

  1. 変更前実装で問題が起こるレアケースを再現するテストを投入する
    テストが失敗するので、何が問題なのか共有できます。
  2. 変更後実装を投入する
    テストが成功するので、直ったような気分になれます。

MYDEVMODE自体はあまり重要なクラスでもないので、直さないとダメか?っていうとそうでもないんですが・・・。

PR のメリット

  • MYDEVMODEの等価比較演算子のバグを解消できます。

PR のデメリット (トレードオフとかあれば)

  • アプリ動作的にはなんの変化もないと思われます。

PR の影響範囲

  • 印刷機能のための内部構造体の比較処理を変更します。
  • アプリ(=サクラエディタ)の機能に影響はありません。

関連チケット

#877 構造体比較にmemcmpを使うのをやめる

参考資料

とくにありません。

@AppVeyorBot
Copy link

Build sakura 1.0.2349 failed (commit a78b14e0e0 by @berryzplus)

@berryzplus berryzplus marked this pull request as ready for review October 24, 2019 21:42
@AppVeyorBot
Copy link

Copy link
Contributor

@beru beru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

変更内容は問題無いと思います。

文字列として扱うべき領域に NULL 終端していないデータを書き込む使い方をしていると等価比較うんぬん以前の問題が起きそうですが…。

@ds14050
Copy link
Contributor

ds14050 commented Oct 26, 2019

つい最近読んだ本に「CLEANコード」という概念がありました。CLEANは頭字語でもあり、A は Assertive(高品質のコードは断定的だ)ということです。「まっすぐで狭いところを歩く(ものごとが意味をなさなくなった瞬間に、道を外れたことに気づけるように)」というアドバイスもありました(別の本でも見かけた文句の気がします)。

このテストは sz (Zero-terminated String) と名前の付いたメンバ変数にヌル文字が1つも存在しないケースを想定し、受け入れ、対処する必要性を示唆するように見受けられますが、その必要性は実在しているのでしょうか。自分は MYDEVMODE 固有の事情は知りませんが、(仮に存在するとして)対処すべきバグはヌル終端していない文字列をメンバとして保持するコードであり、テストは無駄であるばかりか、無用な示唆と疑念をコードを読む人間に与えるものではないかと考えています。

このコメントは @beru さんが将来似たケースで異なる判断を下すといいなあと期待して書いています(迷惑は承知のうえ。期待するのが勝手なら応える応えないも勝手ですので)。

@berryzplus
Copy link
Contributor Author

レビューありがとうございます。
何か問題があれば別PRで対処していきたいと思います。

@berryzplus berryzplus merged commit d7e1061 into sakura-editor:master Oct 26, 2019
@berryzplus berryzplus deleted the feature/improve_equality_of_MYDEVMODE branch October 26, 2019 13:20
@berryzplus
Copy link
Contributor Author

文字列として扱うべき領域に NULL 終端していないデータを書き込む使い方をしていると等価比較うんぬん以前の問題が起きそうですが…。

一応、strncpy的な関数の使い方を誤るとNUL終端されない文字列を作れますので、絶対要らない対応、というわけでもないと思ってました。

このPRは思い付きを出してみた感じのものなので、切迫感はあんまりなかったんですけど 😄

@beru
Copy link
Contributor

beru commented Oct 27, 2019

領域内の文字列データがきちんとNULL終端しているかのチェックを、デバッグビルドの場合に適切な箇所で自動で実行出来れば良いですね。文字列型を介したコードにすれば実現しやすそうですけど、でもちゃんと動いてる箇所を今更書き換えるのも自己満足になってしまうかな。。

C/C++言語はメモリ操作がしやすいですけど意図を外れてメモリ破壊する処理を簡単に書けてしまうので注意が必要ですね。

@beru
Copy link
Contributor

beru commented Oct 27, 2019

つい最近読んだ本に「CLEANコード」という概念がありました。CLEANは頭字語でもあり、A は Assertive(高品質のコードは断定的だ)ということです。「まっすぐで狭いところを歩く(ものごとが意味をなさなくなった瞬間に、道を外れたことに気づけるように)」というアドバイスもありました(別の本でも見かけた文句の気がします)。

関心の分離や単一責任の原則に通じるところがあるんでしょうか?

このテストは sz (Zero-terminated String) と名前の付いたメンバ変数にヌル文字が1つも存在しないケースを想定し、受け入れ、対処する必要性を示唆するように見受けられますが、その必要性は実在しているのでしょうか。自分は MYDEVMODE 固有の事情は知りませんが、(仮に存在するとして)対処すべきバグはヌル終端していない文字列をメンバとして保持するコードであり、テストは無駄であるばかりか、無用な示唆と疑念をコードを読む人間に与えるものではないかと考えています。

文字列領域のメンバーの内容によってそれを保持する型の非等価演算子が正しい動きをするかのテストがこのPRで追加されたわけですが、同じような作りの型が他にたくさんある場合に、それらに対しても同じ内容のテストを追加するとなると骨が折れますね。

そう考えると固定長の領域上をNULL終端文字列として扱う型を用意し、その型のテストで挙動の妥当性を確認するように出来ると良いんでしょうか…。

このコメントは @beru さんが将来似たケースで異なる判断を下すといいなあと期待して書いています(迷惑は承知のうえ。期待するのが勝手なら応える応えないも勝手ですので)。

自分はあんまりチェックせずに気軽に Approve してるので期待に添えるかは微妙ですね…。

@ds14050
Copy link
Contributor

ds14050 commented Oct 27, 2019

もう少し考える材料を提供します。@beru さんはこのテストコードを C++ としてどのように読みましたか。

サイズが一致するだけのでたらめなバイト列を MYDEVMODE のインスタンスであると偽って MYDEVMODE に特化している比較演算子に与え、何か決まった結果を期待するこのコードを。

クラスをまともにクラスとして扱う者には書けないコードです。このコードを支配しているのは、インスタンスのアイデンティティを memcmp によって確かめる精神です。関連チケット に「#877 構造体比較にmemcmpを使うのをやめる」が挙げられているのがとてもおかしい。

@beru
Copy link
Contributor

beru commented Oct 27, 2019

もう少し考える材料を提供します。@beru さんはこのテストコードを C++ としてどのように読みましたか。

サイズが一致するだけのでたらめなバイト列を MYDEVMODE のインスタンスであると偽って MYDEVMODE に特化している比較演算子に与え、何か決まった結果を期待するこのコードを。

クラスをまともにクラスとして扱う者には書けないコードです。このコードを支配しているのは、インスタンスのアイデンティティを memcmp によって確かめる精神です。関連チケット に「#877 構造体比較にmemcmpを使うのをやめる」が挙げられているのがとてもおかしい。

MYDEVMODE にはコンストラクタが無いのできっと律儀にメンバーを個々に初期化する記述をテストコードに入れるのが億劫だったんでしょうね。

しかしテストコードって書くの面倒くさい上にあんまり役立っている気がしないような…とか書いてしまうと他の人の活動にケチをつけているようで良くないですね。。こんな事を言っているとバグを入れた時に今後テスト書く事を強制されてしまう。。ブルブル…。

@ds14050
Copy link
Contributor

ds14050 commented Oct 27, 2019

あ、MYDEVMODE って class ではなくて struct だったんですね。うーん、坊主憎けりゃ袈裟まで憎いだったかなあ。

MYDEVMODE の比較演算子が null 終端されていない文字列を想定し、区別すべきであるとテストコードによって規定するのが、お門違いでなんの足しにもなっていないのは間違いないと考えますが。

@berryzplus
Copy link
Contributor Author

あ、MYDEVMODE って class ではなくて struct だったんですね。うーん、坊主憎けりゃ袈裟まで憎いだったかなあ。

MYDEVMODE の比較演算子が null 終端されていない文字列を想定し、区別すべきであるとテストコードによって規定するのが、お門違いでなんの足しにもなっていないのは間違いないと考えますが。

このままクローズした PR に愚痴られても面倒なので、とりあえず思ったこと書いときます。

何勘違いしてんだこのアフォは。
妙な絡み方してくんじゃねーよ、うぜーから。
と思って放置してました。

ぼくは @Kenchi さんのようにオトナじゃないです。
「思ったこと書いたらどうなるか?」とか考えたくもないです。

「本物は違うぜ!」ってところは見飽きました。
不快なので用がないなら書き込まないでほしいです。
たまに復活するなら @kobake さんがいいです。

以上。

@ds14050
Copy link
Contributor

ds14050 commented Oct 27, 2019

きっかけは「(潜在)バグ認定」です。

#1010 (comment) には全面的に同意しますが、この PR は違います。NULL 文字が入っているべきとき、ところに、それ以外の文字を許容し、有効文字列長を1文字分だけ伸ばすことがあるべき動作だとは思いません。バグってるのはどっちなんだって話です。百歩譲ってどちらもアリだとしても、やっているのはバグ修正ではなく仕様変更です。バグったコードは存在していなかった。

他にもことあるごとにサクラエディタのコードベースがうんこだと吹聴するのを不快に思っています。

@beru
Copy link
Contributor

beru commented Oct 27, 2019

あ、MYDEVMODE って class ではなくて struct だったんですね。うーん、坊主憎けりゃ袈裟まで憎いだったかなあ。

C++ の class と struct の差異はメンバーのアクセス修飾子がデフォルトで public か private かですけれど、MYDEVMODE 型のメンバーって全部POD型なので memset で初期化する事がアウトなわけではないと思います。

MYDEVMODE の比較演算子が null 終端されていない文字列を想定し、区別すべきであるとテストコードによって規定するのが、お門違いでなんの足しにもなっていないのは間違いないと考えますが。

まぁでも MYDEVMODE のインスタンスって領域の初期化さえちゃんとやってれば memcmp で比較しても良いんじゃないかなぁ。。たくさんあるメンバーの1つ1つを突き合わせる記述を書くのとか億劫なので。。まぁ一度書いてしまってメンバー追加しないならそれ以上労力が掛からないだろうから書く手間を惜しむのも良くないかもしれないですけど。。まぁこんな考えでいるとバグに苦しめられるかな。。。

コードやその書き手に関しては、リアルのプロジェクトでコンスタントに実害を受け続けてるとかじゃない限り、そこまで敵視しないでも良いんじゃないかなぁと思いました。

@berryzplus
Copy link
Contributor Author

berryzplus commented Oct 27, 2019

きっかけは「(潜在)バグ認定」です。

#1010 (comment) には全面的に同意しますが、この PR は違います。NULL 文字が入っているべきとき、ところに、それ以外の文字を許容し、有効文字列長を1文字分だけ伸ばすことがあるべき動作だとは思いません。バグってるのはどっちなんだって話です。百歩譲ってどちらもアリだとしても、やっているのはバグ修正ではなく仕様変更です。バグったコードは存在していなかった。

他にもことあるごとにサクラエディタのコードベースがうんこだと吹聴するのを不快に思っています。

強いなぁ、相変わらず・・・そういうとこ好きですが。

バグ認定した対象は、ぼくが書いたコード です。
それが分かるように、一見無関係に思える #877 を関連PRに入れてました。

サクラエディタの設計が一部テキトーな感じになっているのは事実です。
事実はそれなりに認めないと「はだかの王様」です。

「うんこコード」という言い方が不快であるなら、それはすまんです。
うんこコード、放置しててすまんです。

@ds14050
Copy link
Contributor

ds14050 commented Oct 27, 2019

memcmp で比較しても良いんじゃないかなぁ

memset で初期化する前提付きでそれは否定しません。その結果 null 文字(※あるかもしれないしないかもしれない)の後ろのゴミデータの影響を受けるようになるよ、というのもコストとパフォーマンスを天秤にかけられます。

この PR に天秤にかけられるパフォーマンスがあるのか、ということです。

コードやその書き手に関しては、リアルのプロジェクトでコンスタントに実害を受け続けてるとかじゃない限り、そこまで敵視しないでも良いんじゃないかなぁと思いました。

優しいですね。自分はそれに甘えていますが、それも終わりにした方が良さそうです。

@berryzplus
Copy link
Contributor Author

memset で初期化する前提付きでそれは否定しません。その結果 null 文字(※あるかもしれないしないかもしれない)の後ろのゴミデータの影響を受けるようになるよ、というのもコストとパフォーマンスを天秤にかけられます。

この PR に天秤にかけられるパフォーマンスがあるのか、ということです。

やっぱり勘違いしてると思いますよ。

このPRがやってることは、

char buffer[5]; // [in] char* other
if (0 == strncmp(buffer, other, 4)) ....
を
if (0 == strncmp(buffer, other, 5)) ....
とする変更です。

通常buffer[4]にはNULが入ります。
#877 では比較する意味なし、として先頭4文字だけを比較してた感じです。
このPRではNUL文字を含めた5文字分を比較するように修正してます。

後ろのゴミデータの影響を受ける余地はありません。
1文字分増えたことによるパフォーマンス劣化は無視できるレベルと思います。

@ds14050
Copy link
Contributor

ds14050 commented Oct 28, 2019

性分ですので訂正せずにはいられません。

やっぱり勘違いしてると思いますよ。

memcmp で実装するなら(※これが以前の実装です)、パディングやヌル文字以降のゴミデータの影響を避けるために初期化や、メンバの上書きに制約が付きますが、実装は簡単簡潔です。MYDEVMODE はそれを許す C でおなじみの POD 構造体です。

この PR がやっていることは、手間をかけて紛らわしい仕様をテストにより規定する行為です。簡単な実装を選んだがゆえに取り扱いに注意を要するという以前とは異なるが、かといってヌル末端文字列に対する当たり前の期待に応えるようにもなっていません。手をかけて何をやっているんだというのが「Confused」アイコンの意味です。

@ds14050
Copy link
Contributor

ds14050 commented Oct 29, 2019

ACK がないので念のためにもう一度書きます。これは自分の性分であり自分の満足のために行う、国語の時間です。

memset で初期化する前提付きでそれは否定しません。その結果 null 文字(※あるかもしれないしないかもしれない)の後ろのゴミデータの影響を受けるようになるよ、というのもコストとパフォーマンスを天秤にかけられます。

この PR に天秤にかけられるパフォーマンスがあるのか、ということです。

第一段落と第二段落は対比関係にあります。第一段落が memcmp を使った比較について、第二がこの PR による実装について。

memcmp による構造体の比較がヌル文字の存在に頓着せず、ゴミデータの影響さえも受けるのは実装の簡便さを優先したがゆえの制約です。

memcmp の使用をやめてからはヌル文字を要求し、ゴミデータの影響を受けなくなりました。

この PR はヌル文字の要求を放棄しました。ヌル文字を保証しながらテストに期待される比較演算結果を得ようとすると、バッファが1バイト文字分足りなくなるからです(※これが理論上の話なのは承知していますが、エッジケースを無視するプログラマは~略)。簡便さを失っているのに sz と名前の付く変数に対する当たり前の期待を拒絶します。他の二つに比べて得るものがないと第二段落で言っているのです。

@berryzplus
Copy link
Contributor Author

berryzplus commented Nov 2, 2019

具体的に何が困るのか分からんです。

もしかしてこういうケースを想定してます?
lhs: ●●\0●\0
rhs: ●●\0●●

  • 構造体比較にmemcmpを使うのをやめる #877 では 4文字だけ を比較するように変えた。
  • このPRによって 5文字 を比較するように戻した。
  • この状況なら wchar_t[5] の 5文字 ではなくて NUL終端 の位置を考えて 3文字 を比較すべきだと思います。

なんとなく、これとは違うことを指摘しようとしてる気がしてます。
これはこれで問題あるなぁ…と思ってるのでそのうち訂正PRするつもりですが。

@ds14050
Copy link
Contributor

ds14050 commented Nov 18, 2019

成立しない理解と対話に一方的に消耗するのが業腹だと今まで未読スルーしていました。すみません。指摘すべき点があります。

このPRによって 5文字 を比較するように戻した。

  1. \n は \0 の間違いだと思いました。
  2. wcsncmp は文字列比較関数なので3文字目までしか比較しません。
  3. なので引用部は事実誤認ではありませんか?
  4. その上で、改めて考えてみて下さい。

「バッファの末尾の一文字を比較対象にして区別できなければいけないとテストにより規定すること(※この PR に対する自分の理解)」はヌル終端を保証するコードを書くことを妨げ、ヌル終端されていることを期待したコードを潜在的にバグを抱えたコードへと変貌させます。

@berryzplus
Copy link
Contributor Author

ああ、すんません、

¥nは¥0の誤記なのは指摘の通りです。

@berryzplus
Copy link
Contributor Author

で、それはそれとして。

何が問題だと言ってるか、読み取れませんでした。

@berryzplus
Copy link
Contributor Author

  1. wcsncmp は文字列比較関数なので3文字目までしか比較しません。

これ本当?

@ds14050
Copy link
Contributor

ds14050 commented Nov 18, 2019

これ本当?

#include <string.h>
#include <iostream>

int main()
{
	std::cout << wcsncmp(L"abc\0AAAAAAAAAA",L"abc\0ZZZZZZZZZZZ", 7) << std::endl;
	return 0;
}

を実行したところ 0 が出力されました。

@k-takata
Copy link
Member

\0 を含めて比較するなら (w)memcmp を使わないといけませんね。

@ds14050
Copy link
Contributor

ds14050 commented Nov 18, 2019

で、それはそれとして。

何が問題だと言ってるか、読み取れませんでした。

たしかに wcsncmp の仕様の詳細は「それはそれ」です。

すでに書いた通り、「「バッファの末尾の一文字を比較対象にして区別できなければいけないとテストにより規定すること(※この PR に対する自分の理解)」はヌル終端を保証するコードを書くことを妨げ、ヌル終端されていることを期待したコードを潜在的にバグを抱えたコードへと変貌させます。」がすべてです。

この PR をマージする前に sz メンバが利用されているすべての場所を調べ、ヌル文字の存在を前提としたコードが書かれていないかを確かめ、書かれていたならヌル文字が存在しなくても問題がないようにプログラムを修正する準備があったでしょうか。

ヌル終端があるのかないのか、ヌル終端があると期待して良いのかダメなのか、はっきりさせなければバグと無駄のないコードは書けません。Assertive なコードが求められる所以です。テストコードから判断する限りヌル終端は保証されません。テストを満足させながらヌル終端を必ず付けることはできません。ヌル終端のあるなしで構造体が異なるものと判断されるために勝手にヌル終端を付けることは構造体利用者の意図しないデータの改変になります。テストによりヌル終端がない場合があるとはっきり示され、どのように振る舞うべきかが定義されているために、構造体でデータを交換するものの間でたとえそれが暗黙的なものであってもヌル終端を期待することが難しくなっています。それがコードから読み取れることです。ある瞬間に大臣の職にあっただけの者が「新法をそのように悪用するつもりはない」と言明したところで、後世は法に従うのみで法に書かれていることがすべてです。コードが法です。

@ds14050
Copy link
Contributor

ds14050 commented Nov 18, 2019

\0 を含めて比較するなら (w)memcmp を使わないといけませんね。

当初の実装は wmemcmp を使っていてヌル文字の後ろも、ついでにいえば構造体のパディングも比較対象にしていたようですが、それはあるべき仕様というよりは実装の簡便さをとった結果だと理解しています。

なので wmemcmp を廃して wcsncmp を使うようにした「関連チケット #877」は前進であり、今また wmemcmp を使うとすればそれは後退だと思います。

指摘は事実ですがそれを望んでいる者はいないのではないでしょうか。

@berryzplus
Copy link
Contributor Author

berryzplus commented Nov 19, 2019

#1079 (comment) thanks!

自分でもやってみて、wcsncmpの挙動に誤解があったことを把握しました。

やってみたテスト

#include <wchar.h>
#include <gtest/gtest.h>

TEST(CrtTest, test_for_wcsncmp)
{
const wchar_t lhs[]{ L"xx\0x\0" };
const wchar_t rhs[]{ L"xx\0y\n" };

EXPECT_EQ(0, wcsncmp(lhs, rhs, 3));
EXPECT_EQ(0, wcsncmp(lhs, rhs, 4));
EXPECT_EQ(0, wcsncmp(lhs, rhs, 5));
EXPECT_EQ(0, wcsncmp(lhs, rhs, 6));
EXPECT_EQ(0, wcsncmp(lhs, rhs, 7));

}

で、指摘のポイントがよくわかってないんですけど、結局 #1093 等価比較演算子の実装がラクになる方法を探ってみる によって、「一連のテスト追加&修正を反故にしてはどうか?」の提案をしています。標準ライブラリをうまく使えば、詳細部分の実装を「自分で書かない」ことができるので。

@ds14050
Copy link
Contributor

ds14050 commented Nov 19, 2019

結局 #1093 等価比較演算子の実装がラクになる方法を探ってみる によって、「一連のテスト追加&修正を反故にしてはどうか?」の提案をしています。標準ライブラリをうまく使えば、詳細部分の実装を「自分で書かない」ことができるので。

#1093 は内容を見る前にこちらの PR への言及から飛んできてしまいました。

そちらの PR でテストが失敗しているのはこの PR が追加したテストケースが認められないもので不適切だからですよね。それがこの PR に疑問符を付けた理由です。

ヌル終端が存在するという当たり前の期待ができなくなる、ヌル終端を保証するコードを書くとテストが通らなくなる、「正しい」コードが書けなくなる、という理由です。

ヌル終端が存在しないケースを扱えるということも「間違った」コードではありませんが当たり前ではないので、構造体のドキュメントに特記が必要になると思いますし、Windows がそういう値をよこしてくるという必要性に迫られているのでないかぎり無用だと思います。

@ds14050
Copy link
Contributor

ds14050 commented Nov 19, 2019

#1093 の話題ですが分散しないようにこちらへ。string_view を試してみました。以下のようなコードです。

#include <string.h>
#include <string_view>
#include <iostream>

int main()
{
	const wchar_t A[] = L"___AAAAAAA";
	const wchar_t Z[] = L"___ZZZZZZZ";
	const wchar_t(&a)[3] = reinterpret_cast<const wchar_t(&)[3]>(A);
	const wchar_t(&z)[3] = reinterpret_cast<const wchar_t(&)[3]>(Z);
	std::wstring_view av = a;
	std::wcout << L"_countof(a) = " << _countof(a) << L";\n";
	std::wcout << L"wcsncmp(a,z,_countof(a)) = " << wcsncmp(a,z,_countof(a)) << L";\n";
	std::wcout << L"(av == z) = " << (av == z ? L"true":L"false") << L";\n";
	std::wcout << L"av.length() = " << av.length() << L";\n";
	return 0;
}

出力が下の通り。

_countof(a) = 3;
wcsncmp(a,z,_countof(a)) = 0;
(av == z) = false;
av.length() = 10;

wcsncmp を使っていたときにはあった、ヌル終端されていないバッドケースとバッドケースを比較した際に暴走しないための保険が、string_view を使うと失われます。

しかしそういう保険を積極的に掛けようとしているのは他ならぬ @berryzplus さんなので、自分で OK を出すなら反対する人はいないかもしれないですね。

@ds14050
Copy link
Contributor

ds14050 commented Nov 19, 2019

パラノイア気味かとも思いますが、エッジケースにおいてヌル文字が書き込まれたり書き込まれなかったりする文字列関数を呼び出した後、自分でヌル文字をバッファの最後に書き込むことがあります。

ドキュメントを正しく理解できているか、ランタイムによる差異がないか、ランタイムのバージョンによる差異がないか、色々考えるより自分で保証する方が安心確実だからです。

string_view を利用する場合でも、事前に無条件にバッファの末尾にヌル文字を書き込んでおくことで wcsnlen と assert を組み合わせるのとは違った形の安全策になります。それをするかしないかは「パラノイア」の度合い次第ですが。

このような安全策をとる可能性を潰すから、この PR が追加するテストケースとそれに合わせた実装の変更に疑問符を付けたのです。

@berryzplus
Copy link
Contributor Author

いったん整理。

関連チケット

#877 構造体比較にmemcmpを使うのをやめる
#1079 MYDEVMODEの等価比較演算子の隠れバグを修正する
#1093 等価比較演算子の実装がラクになる方法を探ってみる

修正前の模擬コード

struct AAA {
  wchar_t aa[3];
  wchar_t bb[3];
  bool operator == (const AAA& rhs) const {
    return 0 == wmemcmp(aa, rhs, _countof(AAA));
  }
};

AAA aaa{ {L"aa"}, {L"bb"} };
wchar_t* szBbb = L"aa\0bb\0";
AAA& bbb = *reinterpret_cast<AAA*>(szBbb);
ASSERT_EQ(aaa, bbb);
  • 従来、型AAAの比較には(w)memcmpを使っていた。
  • 実際には演算子は存在していなかったが、比較のために演算子として書いた。

#877が解決した問題

struct AAA {
  wchar_t aa[3];
  wchar_t bb[3];
  bool operator == (const AAA& rhs) const {
    return 0 == wcsncmp(aa, rhs.aa, _countof(aa) - 1)
      && 0 == wcsncmp(bb, rhs.bb, _countof(bb) - 1);
  }
};

AAA aaa{ {L"aa"}, {L"bb"} };
wchar_t* szBbb = L"aa\0bb\0";
AAA& bbb = *reinterpret_cast<AAA*>(szBbb);
ASSERT_EQ(aaa, bbb);
  • 個々のメンバで比較を行うように改善した。
    • 未使用領域の差異により、意図せず「不一致」となる事態の防止が目的。
    • wcscmpではなく wcsncmpを使う方が安全 という指摘があったので変更した。
      • ★1 wcsncmp を使わなかった場合に何が起きるかを示すため StrategyForSegmentationFault を追加した。
      • ★2 wcsncmp を導入する際、_countof(AAA::aa) - 1 を検査範囲とした。

★1 のケースが「必要か?」というと別に要らんと思っています。
★2 について、検査範囲の訂正が必要でした。⇒ #1079

#1079が解決した問題

struct AAA {
  wchar_t aa[3];
  wchar_t bb[3];
  bool operator == (const AAA& rhs) const {
    return 0 == wcsncmp(aa, rhs.aa, _countof(aa))
      && 0 == wcsncmp(bb, rhs.bb, _countof(bb));
  }
};

AAA aaa{ {L"aa"}, {L"bb"} };
wchar_t* szBbb = L"aa\0bb\0";
AAA& bbb = *reinterpret_cast<AAA*>(szBbb);
ASSERT_EQ(aaa, bbb);
wchar_t* szCcc = L"aaabb\0";
AAA& ccc = *reinterpret_cast<AAA*>(szCcc);
ASSERT_NE(aaa, ccc);
  • 文字列の最終要素を無視するのをやめた。
  • 作成したテストケースに無理があるのは事実。
  • AAA::aa に 3文字以上 の文字列をコピーするのを防ぐのは無理ゲー。
    • やるならカプセル化だけど、メリットに対して変更量が多くなり過ぎの気がする。

#1093 の提案内容

struct AAA {
  wchar_t aa[3];
  wchar_t bb[3];
  bool operator == (const AAA& rhs) const {
    return std::wstring_view(aa) == rhs.aa
      && std::wstring_view(bb) == rhs.bb;
  }
  bool operator != (const AAA& rhs) const {
    return !(*this == rhs);
  }
};

AAA aaa{ {L"aa"}, {L"bb"} };
wchar_t* szBbb = L"aa\0bb\0";
AAA& bbb = *reinterpret_cast<AAA*>(szBbb);
ASSERT_EQ(aaa, bbb);
wchar_t* szCcc = L"aaabb\0";
AAA& ccc = *reinterpret_cast<AAA*>(szCcc);
ASSERT_NE(aaa, ccc);
  • 文字列比較に標準ライブラリを使うように変更。
  • 比較対象のポインタがNUL終端されていない(=文字列ではない)場合、落ちることがある。

#1093 の提案内容を一歩進めてみる

struct AAA {
  wchar_t aa[3];
  wchar_t bb[3];
  bool operator == (const AAA& rhs) const {
    return 0 == std::wstring_view(aa).compare(rhs.aa)
      && 0 == std::wstring_view(bb).compare(rhs.bb);
  }
  bool operator != (const AAA& rhs) const {
    return !(*this == rhs);
  }
};

AAA aaa{ {L"aa"}, {L"bb"} };
wchar_t* szBbb = L"aa\0bb\0";
AAA& bbb = *reinterpret_cast<AAA*>(szBbb);
ASSERT_EQ(aaa, bbb);
wchar_t* szCcc = L"aaabb\0";
AAA& ccc = *reinterpret_cast<AAA*>(szCcc);
ASSERT_NE(aaa, ccc);
  • lhs側のデータがまとも(=NUL終端されている)な限り、落ちることがなくなる。
  • std::char_traits<wchar_t>::length() の呼出回数が減らせる

なお、 std::wstring_view を使う話は、 @beru さんがSTLコンテナアレルギーを発症する懸念があるので、流れるかもしんない・・・。

@ds14050
Copy link
Contributor

ds14050 commented Nov 22, 2019

#877 構造体比較にmemcmpを使うのをやめる」の時点ですでに悪しき仕様がテストに織り込まれていたんですね。気がついていませんでした。

例外条件で落ちないことを確認するのに ASSERT はいりません。悪条件を用意した後で演算子を呼び出すだけでいいんです。ただし結果は捨てます。特定の結果を要求すると、例外条件が正常ケースになります。そこから無理が生じています。

@berryzplus
Copy link
Contributor Author

#877 構造体比較にmemcmpを使うのをやめる」の時点ですでに悪しき仕様がテストに織り込まれていたんですね。気がついていませんでした。

気付いてなかったならしょうがない・・・

何をしても落ちないを「悪しき仕様」と言われてる気がしてなんか引っかかりました。

MYDEVMODEの等価比較演算子には noexcept 指定をつけてあります。
noexcept は、 絶対に例外を投げない という仕様表明です。
「関数が絶対に例外を投げないこと」によるメリットは noexcept の説明にある通りです。

なお、変数名のプレフィックスszの意味は、 分かってないやつのが多い と思います。
sz = zero terminated string で、 NUL終端された文字列 と同義です。
※zero(英語) = null(ドイツ語) = 0(数字) = NUL(ASCIIの文字番号0に振られた文字名)

@ds14050
Copy link
Contributor

ds14050 commented Nov 24, 2019

@berryzplus さんの意見が 日本語で 聞きたいです。MYDEVMODE の文字列メンバのヌル終端に関する仕様です。実装とテストを用意するにあたり、どういう仕様を頭の中で定めているのかです。べき論や理想ではなく、現実としてどういう仕様を想定して、実装とテストを用意しているかです。


余談。

入力次第で例外を投げるかもしれない関数に noexcept を付けるなら noexcept は飾りです。もしくは C++ の言語仕様としての例外処理が飾りになります。

sz の意味がわかっている人は、sz 接頭辞がついたメンバにヌル文字の存在を期待します。ヌル文字が存在しない場合にどういう比較結果を返すべきかは不明です。穏当な解決策は、実際に書き込むか書き込まないかはオプションですが、ヌル文字が存在しているかのように比較することです。ヌル文字が存在しない場合を許容し、バッファの全長に渡り辞書式の比較結果を返すことを約束するなら、sz 接頭辞は嘘です。

能書きを過不足なくコードで表現することが肝要です。しかし母語が不自由な人間は、プログラミング言語を操ることにも不自由するでしょう。

@ds14050
Copy link
Contributor

ds14050 commented Nov 24, 2019

以下の通り理解しました。

  • MYDEVMODE の「より妥当な比較結果」に関心がない。ヌル文字があるとかないとかどっちでもいい。ただしパディングだけは気にしてみた>構造体比較にmemcmpを使うのをやめる #877
  • wcsncmp の n に象徴される安全策に必要性を感じない。デステストも用意してみただけ。
  • ヌル文字が存在しないせいで MYDEVMODE を使用する場所で落ちてもそれは最初からある別の問題。そもそもヌル文字が存在しないケースが現実的でない。

それでは済まされないと考えてしまう理由がこれです。

  • テストが現在の実装(※メンバ変数の名前が最たるもの)と食い違っている。
  • テストが未来の実装を縛る。

テストが存在し、ASSERT により求められる結果が定義された時点で、「どっちでもいい」がどっちでも良くなくなっているんです。そしてその内容が良くないものだから、テストがない方がマシなのです。

しかしこんなのはきっかけの部分に勘違いがあったが故です。他人が差し出口をしました。

@berryzplus
Copy link
Contributor Author

@berryzplus さんの意見が 日本語で 聞きたいです。
MYDEVMODE の文字列メンバのヌル終端に関する仕様です。
実装とテストを用意するにあたり、どういう仕様を頭の中で定めているのかです。
べき論や理想ではなく、現実としてどういう仕様を想定して、実装とテストを用意しているかです。

現実として、MYDEVMODE は C言語 の構造体なので、各メンバ変数の名前は、メンバの識別以外の意味を持たないと考えています。当然 sz プレフィックスは嘘です。外部から容易にアクセス可能な、ただの連続データの一部領域に書き込むデータに制約をかける方法はありません。

演算子の実装仕様(文字列メンバの比較について)

  • 文字列メンバは文字型の配列変数なので、最初にNUL文字が現れるまでを比較します。
    • ただし、配列の領域終端までNUL文字が現れない場合、配列領域の終端で比較を打ち切ります。
    • 比較を打ち切った場合の判定は、「該当メンバは等しい」となります。

役割は「あくまで比較」で「妥当性の検証」ではないので、問題ないと思います。
※この文章が「日本語じゃない」と思う人がいたら容赦なく突っ込んでください...orz

内容の8割がプログラミング専門用語で構成される文章を日本語扱いしていいのか? みたいな倫理的課題は別にして。

演算子のテストの実装仕様

  • 等価なオブジェクトを等価と判定できること。
  • 等価でないオブジェクトを等価でないと判定できること。
  • (オプション) 異常なデータを渡した場合でも例外を発生させないこと。

しかし母語が不自由な人間は、プログラミング言語を操ることにも不自由するでしょう。

キミがそれを言うなよ(核爆 と返されることを織り込んだうえでギャグ書いてるのか、
「やったらやり返される」って会話の基本さえ理解できないアフォの子なのか。

このやりとりは、キミがぼくに対して「何が問題なのか」を正しく伝えられないがために続いていると思っています。「日本語がダメならコードで書いてくれてもいいんだよ」というのは、ぼくも言ってることで、それすらできないから話が終わんないんだと思います。

@ds14050
Copy link
Contributor

ds14050 commented Nov 24, 2019

コードにより保証できないから、テストにより、ヌル終端があるという暗黙の前提を許さないということですね。たしかにこの PR に含まれる比較演算実装の修正は、それに沿うものです。

テストにより規定されるこの新しい状況では、比較演算実装に wcscmp(n がないバージョン)や string_view を使うことが許されません。アクセス違反が避けられないからです。

ではどうして「wcscmpではなく wcsncmpを使う方が安全 という指摘があったので変更した。 ★1 wcsncmp を使わなかった場合に何が起きるかを示すため StrategyForSegmentationFault を追加した。 ★1 のケースが「必要か?」というと別に要らんと思っています。」というような矛盾した言動が出てくるのでしょうか。

どうして「#1093 等価比較演算子の実装がラクになる方法を探ってみる」のような string_view を使う PR を後から出してきて、テストを撤回しようとするのでしょうか。

何をやっているのでしょうか。自分がついさっきまでどちらを向いて進んでいたかも理解できていないから、右往左往するのです。

@berryzplus
Copy link
Contributor Author

ではどうして「wcscmpではなく wcsncmpを使う方が安全 という指摘があったので変更した。 ★1 wcsncmp を使わなかった場合に何が起きるかを示すため StrategyForSegmentationFault を追加した。 ★1 のケースが「必要か?」というと別に要らんと思っています。」というような矛盾した言動が出てくるのでしょうか。

ぼくが人間だからです。
マシーンじゃないので言動がブレることくらいあります。

どうして「#1093 等価比較演算子の実装がラクになる方法を探ってみる」のような string_view を使う PR を後から出してきて、テストを撤回しようとするのでしょうか。

ぼくが人間だからです。
自分自身の了見の狭さが打開策の判断を誤る原因になることも、たまにはあります。

何をやっているのでしょうか。

最善策の模索です。

やってみないと分からないことはあるし、
やってみせないと得られない理解もあると思っています。

右往左往してるのをあえてみせたことで、bool operator == (const MYDEVMODE& rhs) const noexcept の意味する仕様について理解が深まりませんでしたか?

@ds14050
Copy link
Contributor

ds14050 commented Nov 24, 2019

開き直りとはいえはじめて認めましたね。よくできました。

@m-tmatma m-tmatma added this to the v2.4.0 milestone Dec 29, 2019
@KENCHjp KENCHjp added the 🐛bug🦋 ■バグ修正(Something isn't working) label Jan 7, 2020
HoppingTappy pushed a commit to HoppingTappy/sakura that referenced this pull request Jun 16, 2020
…_equality_of_MYDEVMODE

MYDEVMODEの等価比較演算子の隠れバグを修正する
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛bug🦋 ■バグ修正(Something isn't working)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants