#include <iostream>
using namespace std;
class Foo
{
public:
Foo(): initialised(0)
{
cout << "Foo() gets called AFTER test() ?!" << endl;
};
Foo test()
{
cout << "initialised= " << initialised << " ?! - ";
cout << "but I expect it to be 0 from the 'initialised(0)' initialiser on Foo()" << endl;
cout << "this method test() is clearly working on an uninitialised object ?!" << endl;
return Foo();
}
~Foo()
{};
private:
int initialised;
};
int main()
{
//SURE this is bad coding but it compiles and runs
//I want my class to DETECT and THROW an error to prevent this type of coding
//in other words how to catch it at run time and throw "not initialised" or something
Foo foo=foo.test();
}
4 に答える
はい、まだ構築されていないオブジェクトで関数を呼び出していますが、これは未定義の動作です。信頼できる検出はできません。また、それを検出しようとするべきではないと主張します。たとえば、すでに削除されたオブジェクトで関数を呼び出す場合と比較して、偶然に起こる可能性はありません。考えられるすべての間違いを見つけようとすることは、ほぼ不可能です。宣言された名前は、他の有用な目的のために、その初期化子で既に表示されています。このことを考慮:
Type *t = (Type*)malloc(sizeof(*t));
これは C プログラミングの一般的なイディオムであり、C++ でも機能します。
個人的には、Herb Sutter による null 参照 (同様に無効) に関するこの話が好きです。要点は、言語が明らかに禁止しているケースから保護しようとしないでください。特に、一般的なケースでは確実に診断することが不可能です。時間の経過とともに偽のセキュリティが得られ、非常に危険になります. 代わりに、間違いを犯す可能性を減らす方法 (生のポインターを避けるなど) で、言語と設計インターフェイスの理解を訓練してください。
C++ および同様に C では、多くのケースが明示的に禁止されておらず、未定義のままになっています。部分的には効率的に診断するのがかなり難しいため、部分的には未定義の動作により、実装がそれを完全に無視するのではなく、代替動作を設計できるためです。これは既存のコンパイラでよく使用されます。
たとえば、上記の場合、どの実装でも自由に例外をスローできます。実装のために効率的に診断するのがはるかに難しい、同様に未定義の動作である他の状況があります。オブジェクトが構築される前にアクセスされる別の翻訳単位にあるオブジェクトを持つことは、そのような例です-これは静的初期化順序フィアスコとして知られています。
コンストラクターは、必要なメソッドです (初期化前ではなく、初期化時に実行されますが、それで問題ありません)。あなたの場合にうまくいかない理由は、ここで未定義の動作があるためです。
特に、まだ存在しない foo オブジェクトを使用してそれ自体を初期化します (たとえば、foo
infoo.Test()
はまだ存在しません)。オブジェクトを明示的に作成することで解決できます。
Foo foo=Foo().test()
プログラムでチェックすることはできませんが、おそらく valgrind がこのタイプのバグを見つけることができます (他の初期化されていないメモリ アクセスと同様)。
人々が下手なコーディングをするのを防ぐことはできません。「本来あるべき」ように機能します。
- Foo (「this」ポインターの値) にメモリーを割り当てます。
- 次のようにして Foo::test に移動します: Foo::test(this)。
- this->initialized によって値を取得します。これはランダムなジャンクです。
- Foo のデフォルト コンストラクターを呼び出します (return Foo(); のため)。
- 右利きの Foo() をコピーするには、Foo のコピー コンストラクターを呼び出します。
まさにそうあるべきです。C++ の正しい使い方を知らない人を防ぐことはできません。
あなたができる最善のことは、魔法の数を持つことです:
class A
{
public:
A(void) :
_magicFlag(1337)
{
}
void some_method(void)
{
assert (_magicFlag == 1337); /* make sure the constructor has been called */
}
private:
unsigned _magicFlag;
}
値がすでに 1337 である場所に _magicFlag が割り当てられる可能性が低いため、これは「機能します」。
しかし、本当に、これをしないでください。
基本的に「コンパイラがこれを手伝ってくれると期待すべきではない」というかなりの数の応答を受け取っています。ただし、コンパイラが何らかの診断によってこの問題を解決する必要があることに同意します。残念ながら(他の回答が指摘しているように)、言語仕様はここでは役に立ちません-宣言の初期化子部分に到達すると、新しく宣言された識別子がスコープ内になります。
少し前に、DDJ は「DogTag」と呼ばれる簡単なデバッグ クラスについての記事を掲載しました。
- 削除後のオブジェクトの使用
- オブジェクトのメモリをガベージで上書きする
- オブジェクトを初期化する前に使用する
私はあまり使用していませんが、メモリ上書きバグが発生していた組み込みプロジェクトで重宝しました。
これは基本的に、GMan が説明した「MagicFlag」テクニックの精緻化です。