私はかなり複雑な数学ライブラリに取り組んでいますが、クライアント コードで auto を使用すると厄介なバグが発生することを発見しました。それについて質問するために最小限の再現ケースを作成している途中で、標準ライブラリのみを使用して同様のものを再現できることに気付きました。次の簡単なテスト ケースをご覧ください。
#include <vector>
#include <assert.h>
int main()
{
std::vector<bool> allTheData = {true, false, true};
auto boolValue = allTheData[1]; // This should be false - we just declared it.
assert(boolValue == false);
boolValue = !boolValue;
assert(boolValue == true);
assert(allTheData[1] == false); // Huh? But we never changed the source data! Only our local copy.
}
ゴッドボルトでライブ(面白い事実:Clang は実際にこれを「7」(3 つの true ビット)の書き込みと __assert_fail の呼び出しに最適化します。)
(はい、std::vector<bool> がダメなのはわかっています- しかし、この場合は、数行だけの最小限の再現可能な例を作成するのが便利です)ここにstd::vector<bool> を使用しない長い例、割り当てとコピー/移動が削除されたカスタム コンテナー タイプを使用していますが、それでも問題は発生します。
内部で何が起こっているかは理解しています。operator[] によって返されるプロキシ クラスは、allTheData[1] = true
関連する機能を実装することを意図しており、値を読み取っているかのように記述されたクライアント コードは、実際にはプロキシを boolValue に格納しています。その後、クライアントが bool であると考えているものを変更すると、元のソース データが変更されます。TLDR: 「自動」でプロキシをコピーしました。
コードは、プログラマーが意図した動作ではなく、プログラマーが指示した動作を実行しました。
プログラマーが boolValue の変更によってソースデータを更新したい場合、 を実行します。auto& boolValue = ...
これは、operator[]
を返す実装では機能しますT&
が、参照のような動作を偽装するカスタムプロキシを必要とする実装では機能しません。
プロキシのすべてのコピーおよび移動コンストラクターと、両方の代入演算子はプライベートとして宣言されています ( も試しました= delete
) が、このバグはコンパイル時に検出されません。コピー コンストラクターが削除されるかどうかに関係なく、プロキシはコピーされます。
このバグに対して私が見つけた「修正」はすべて、コードのクライアント部分に焦点を当てています。たとえば、「auto を使用しない」、「基になる型にキャストする」、「const ref を介してアクセスする」などです。これらはすべて標準以下の修正であり、悪い動作を発見したら、ハック修正としてこれらのいずれかを追加できますが、根本的な問題は、次の疑いを持たないユーザーを捕まえるために残っています。
地雷を回避し続けるよりは、地雷を除去したいです。「auto を使用しないでください」または「常に const を使用してください」という標識を立てても、地雷原を示すだけで、除去にはなりません。
ライブラリをこの罠から守るにはどうすればよいですか? (クライアント コードを変更せずに!)
- 第一に、コードは記述どおりに動作することが望ましく、
assert(allTheData[1] == false)
合格です 。- プロキシが auto? に書き込まれるときに、プロキシの減衰タイプを定義する方法。?も同様
decltype(boolValue)
です。bool
- コピーよりも優先される暗黙の変換演算子ですか?
- 上記のコード スニペットを変更せずにこれを成功させる他の方法はありますか?
- プロキシが auto? に書き込まれるときに、プロキシの減衰タイプを定義する方法。?も同様
- 2 番目の選択肢として、変数へのプロキシの書き込みをコンパイル エラーにする方法はありますか?
- コピーおよび移動コンストラクタを delete として宣言し、移動およびコピー代入演算子を delete として宣言しています。それでもコンパイルされます。
- クラスが lvalue になれないことを宣言する方法はありますか?
- 提案されている C++ の将来の標準には、これを修正するものはありますか?
また、次のようなコードも問題です:
std::vector<bool> ReadFlags();
... later ...
auto databaseIsLockedFlag = ReadFlags()[FLAG_DB_LOCKED];
if (databaseIsLockedFlag) <-- Crash here. Proxy has outlived temporary vector.
ここでは、問題の非常に単純な例として、ベクターのみを使用しています。これはベクターのバグではなく、プロキシ型パターンのバグであり、ベクターは問題を示す例です。
不思議なことに、MSVCのIntellisenseエンジン時々移動不可、コピー不可のプロキシ型をコピーするとコンパイルエラーとして報告されますが、とにかくうまくコンパイルされます:
ベストアンサー1
プロキシクラスの演算子の末尾に「&&」を追加してダメージを軽減します。
(および演算子 +=、-= など)
何度も実験を重ねましたが、最終的には、最も一般的なケースの問題を軽減する方法を見つけました。これにより問題が厳しくなり、プロキシをコピーすることはできますが、一度スタック変数にコピーすると、それを変更してソース コンテナーを誤って破損することはできなくなります。
#include <cstdio>
#include <utility>
auto someComplexMethod()
{
struct s
{
void operator=(int A)&& {std::printf("Setting A to %i", A);}
};
return s();
}
int main()
{
someComplexMethod() = 4; // Compiles. Yay
auto b = someComplexMethod();
// Unfortunately that still compiles, and it's still taking a
// copy of the proxy, but no damage is done yet.
b = 5;
// That doesn't compile. Error given is:
// No overload for '=' note: candidate function not viable:
// expects an rvalue for object argument
std::move(b) = 6;
// That compiles, but is basically casting around the
// protections, aka shooting yourself in the foot.
}