6

C ++標準によると、オブジェクトが元々それ自体でない場合は、ポインタから離れconstてオブジェクトに書き込むことができます。constだからこれ:

 const Type* object = new Type();
 const_cast<Type*>( object )->Modify();

大丈夫ですが、これは:

 const Type object;
 const_cast<Type*>( &object )->Modify();

UBです。

その理由は、オブジェクト自体がオブジェクトである場合const、コンパイラはそのオブジェクトへのアクセスを最適化できるためです。たとえば、繰り返しの読み取りは変更されないオブジェクトでは意味がないため、繰り返しの読み取りは実行されません。

問題は、コンパイラが実際にどのオブジェクトであるかをどのように知るかということconstです。たとえば、次の関数があります。

void function( const Type* object )
{
    const_cast<Type*>( object )->Modify();
}

そしてそれは静的ライブラリにコンパイルされ、コンパイラはそれがどのオブジェクトに対して呼び出されるかを知りません。

これで、呼び出し元のコードでこれを実行できます。

Type* object = new Type();
function( object );

そしてそれは大丈夫でしょう、またはそれはこれを行うことができます:

const Type object;
function( &object );

そしてそれは未定義の振る舞いになります。

コンパイラはこのような要件にどのように準拠する必要がありますか?後者を機能させずに前者を機能させるにはどうすればよいでしょうか。

4

5 に答える 5

6

「後者を機能させずに前者を機能させるにはどうすればよいのか」と言うとき。実装は前者を機能させるためにのみ必要であり、プログラマーを支援したい場合を除いて、後者を特定の方法で機能させないようにするために特別な努力をする必要はありません。未定義の動作は、義務ではなく、実装に自由を与えます。

より具体的な例を見てみましょう。この例では、コンパイラーは、コンストラクターが完了するとconstであり、その後は書き込まれない可能性があるためf()、呼び出す前に戻り値を10に設定する場合があります。関数だけが呼び出されても、同じ仮定をすることはできません。未定義の動作で呼び出されたときに変更の試みが発生し、実装が後続のアクションに特別な影響を与える必要がない場合。EvilMutatecobj.membercobjg()constEvilMutatemembercobjf()

真のオブジェクトが変更されないと想定するコンパイラーの機能は、変更すると未定義の動作constが発生するという事実によって保護されます。それが行われるという事実は、コンパイラーに追加の要件を課すことはなく、プログラマーにのみ課します。

struct Type {
    int member;
    void Mutate();
    void EvilMutate() const;
    Type() : member(10) {}
};


int f()
{
    const Type cobj;
    cobj.EvilMutate();
    return cobj.member; 
}

int g()
{
     Type obj;
     obj.EvilMutate();
     return obj.member; 
}
于 2011-12-16T10:40:48.217 に答える
3

コンパイラーは、constオブジェクトに対してのみ最適化を実行でき、constオブジェクトへの参照/ポインターに対しては実行できません(この質問を参照)。あなたの例では、コンパイラーが最適化できる方法はありませんが、コンパイラーはfunctionを使用してコードを最適化できますconst Type。このオブジェクトはコンパイラによって一定であると見なされるため、(を呼び出すことによってfunction)変更すると、プログラムのクラッシュ(たとえば、オブジェクトが読み取り専用メモリに格納されている場合)や非constバージョンのように動作する(if変更は最適化に干渉しません)

non-constバージョンは問題なく、完全に定義されています。non-constオブジェクトを変更するだけで、すべてが正常になります。

于 2011-12-16T09:14:40.117 に答える
2

オブジェクトが宣言されている場合const、実装は、オブジェクトを変更しようとするとハードウェアトラップが発生する可能性がある方法でオブジェクトを格納できますが、それらのトラップの特定の動作を保証する義務はありません。そのようなオブジェクトへのポインターを作成する場合const、そのポインターの受信者は通常、それを書き込むことを許可されないため、これらのハードウェアトラップをトリガーする危険はありません。コードがconst-nessをキャストしてポインターに書き込む場合、コンパイラーは、発生する可能性のあるハードウェアの異常からプログラマーを保護する義務を負わないことになります。

さらに、コンパイラがconstオブジェクトに常に特定のバイトシーケンスが含まれることを通知できる場合は、リンカーにそのことを通知し、リンカーがそのバイトシーケンスがコード内のどこかにあるかどうかを確認できるようにします。その場合、オブジェクトのアドレスをそのバイトシーケンスの場所と見なしconstます(一意のアドレスを持つさまざまなオブジェクトに関するさまざまな制限に準拠するのは少し難しいかもしれませんが、許容されます)。コンパイラがリンカーに、const char[4]ある関数のコンパイル済みコード内にたまたま出現したバイトシーケンスが常に含まれているはずであると伝えた場合、リンカーはそのバイトシーケンスが出現するコード内のアドレスをその変数に割り当てることができます。の場合constは書き込まれなかったため、このような動作では4バイトが節約されますが、に書き込むとconst、他のコードの意味が任意に変更されます。

キャストアウェイ後にオブジェクトに書き込むことconstが常にUBである場合、const-nessをキャストアウェイする機能はあまり役に立ちません。このように、この機能は、他のコードの利益のために、コードのconst一部がポインターを保持している状況で役割を果たすことがよくあります。非オブジェクトへのポインターの恒常性を捨てる動作が定義されていない場合、ポインターを保持しているコードは、どのポインターがどのポインターであり、どのポインターを記述する必要があるかを知る必要があります。ただし、const-castingが許可されているため、ポインターを保持するコードは、ポインターをすべて次のように宣言するだけで十分です。constconstconstconst、およびポインタが非定数オブジェクトを識別し、それを書き込みたいことを知っているコードの場合、それを非キャストポインタにキャストします。

constC ++に、ポインターで使用できる(および)修飾子の形式があり、コンパイラーがポインターを識別していると見なすことvolatileができる(または、の場合はそうすべきである)ことをコンパイラーに指示する役立つ場合があります。オブジェクトがであることがわかり、オブジェクトが宣言されていない、および/または宣言されていないことを知っています。前者を使用すると、コンパイラーは、ポインターによって識別されたオブジェクトがポインターの存続期間中に変更されないと想定し、それに基づいてデータをキャッシュできます。後者は、変数がいくつかのまれな状況(通常はプログラムの起動時)でアクセスをサポートする必要があるかもしれないが、コンパイラーがその後その値をキャッシュできるはずである場合を考慮に入れます。しかし、そのような機能を追加する提案はありません。volatileconstvolatileconstvolatilevolatile

于 2015-07-23T16:13:50.910 に答える
1

未定義の動作とは、未定義の動作を意味します。この仕様は、何が起こるかを保証するものではありません。

それはあなたが意図したことをしないという意味ではありません。仕様が機能するはずの動作の境界の外にいるというだけです。仕様は、特定のことをしたときに何が起こるかを示すためにあります。スペックの保護の範囲外では、すべての賭けはオフになっています。

しかし、マップの端から離れているからといって、ドラゴンに遭遇するわけではありません。ふわふわうさぎかもしれません。

このように考えてください:

class BaseClass {};
class Derived : public BaseClass {};

BaseClass *pDerived = new Derived();
BaseClass *pBase = new Base();

Derived *pLegal = static_cast<Derived*>(pDerived);
Derived *pIllegal = static_cast<Derived*>(pBase);

C ++は、これらのキャストの1つを完全に有効であると定義しています。もう1つは、未定義の動作をもたらします。これは、C ++コンパイラが実際に型をチェックし、「未定義動作」スイッチを切り替えることを意味しますか?いいえ。

つまり、C ++コンパイラは、それが実際にはaであると想定する可能性が高いため、をに変換するために必要なポインタ演算を実行します。それが実際にはではない場合、未定義の結果が得られます。pBaseDerivedpBaseDerived*Derived

そのポインタ演算は、実際には何の操作も行わない可能性があります。何もしないかもしれません。またはそれは実際に何かをするかもしれません。それは問題ではありません; これで、仕様で定義された動作の領域外になります。ポインタ演算がノーオペレーションの場合、すべてが完全に機能しているように見える場合があります。

コンパイラが、あるインスタンスでは未定義であり、別のインスタンスでは定義されていることを「認識」しているわけではありません。それは、仕様が何が起こるかを述べていないということです。動作しているように見える場合があります。そうではないかもしれません。それが機能するのは、仕様に従って適切に行われた場合のみです。

同じことがconstキャストにも当てはまります。constキャストが元々ではなかったオブジェクトからのものである場合const、仕様はそれが機能することを示しています。そうでない場合、仕様は何でも起こり得ると言っています。

于 2011-12-16T06:37:18.313 に答える
0

理論的には、constオブジェクトは読み取り専用メモリに格納できる場合があり、オブジェクトを変更しようとすると明らかな問題が発生しますが、より可能性の高いケースは、オブジェクトの定義が表示されている場合です。 、オブジェクトがconstとして定義されていることをコンパイラーが実際に認識できるように、コンパイラーは、そのオブジェクトのメンバーが変更されないという仮定に基づいて最適化できます。constオブジェクトでnon-const関数を呼び出してメンバーを設定し、そのメンバーを読み取る場合、コンパイラーは、値を既に知っている場合、そのメンバーの読み取りをバイパスする可能性があります。結局のところ、オブジェクトをconstとして定義しました。つまり、その値は変更されないことを約束しました。

未定義の動作は、わずかな変更を加えるまで、期待どおりに機能するように見えることが多いという点で注意が必要です。

于 2011-12-16T06:43:51.403 に答える