9

私が書いているプログラムでは、次のパターンが発生しています。あまり工夫されていないことを願っていますが、const_castなどを使用せずFooに、constメソッドでオブジェクトを変更することができます。Foo::Questionable() const基本的に、Fooへの参照を格納しFooOwner、その逆も同様です。では、所有者を呼び出すことによりQuestionable()Fooconstメソッドでそれ自体を変更することができます。mutate_foo()質問はコードに従います。

#include "stdafx.h"
#include <iostream>
using namespace std;

class FooOwner;

class Foo {
    FooOwner& owner;
    int data;

public:
    Foo(FooOwner& owner_, int data_)
        : owner(owner_),
          data(data_)
    {
    }

    void SetData(int data_)
    {
        data = data_;
    }

    int Questionable() const;       // defined after FooOwner
};

class FooOwner {
    Foo* pFoo;

public:
    FooOwner()
        : pFoo(NULL)
    {}

    void own(Foo& foo)
    {
        pFoo = &foo;
    }

    void mutate_foo()
    {
        if (pFoo != NULL)
            pFoo->SetData(0);
    }
};

int Foo::Questionable() const
{
    owner.mutate_foo();     // point of interest
    return data;
}

int main()
{
    FooOwner foo_owner;
    Foo foo(foo_owner, 0);      // foo keeps reference to foo_owner
    foo_owner.own(foo);         // foo_owner keeps pointer to foo

    cout << foo.Questionable() << endl;  // correct?

    return 0;
}

これは定義された動作ですか?Foo::data可変と宣言する必要がありますか?それとも、これは私が致命的に間違ったことをしている兆候ですか?要求されたときにのみ設定される一種の遅延初期化された「データ」を実装しようとしています。次のコードは警告なしで正常にコンパイルされるため、UBランドにいるので少し緊張しています。

編集:conston Questionable()は、直接のメンバーをconstにするだけで、オブジェクトによってポイントまたは参照されるオブジェクトは作成しません。これはコードを合法にしますか?Questionable()thisがタイプconst Foo*を持っているという事実と、コールスタックのさらに下にある、FooOwnerを変更するために使用する非定数ポインタを合法的に持っているという事実の間で混乱していますFooFooこれは、オブジェクトを変更できるかどうかを意味しますか?

編集2:おそらくもっと簡単な例:

class X {
    X* nonconst_this;   // Only turns in to X* const in a const method!
    int data;

public:
    X()
        : nonconst_this(this),
          data(0)
    {
    }

    int GetData() const
    {
        nonconst_this->data = 5;    // legal??
        return data;
    }
};
4

5 に答える 5

27

次のことを考慮してください。

int i = 3;

iはオブジェクトであり、タイプはintです。履歴書の資格がありません(constまたは、、volatileまたはその両方ではありません)。

ここで、次を追加します。

const int& j = i;
const int* k = &i;

jはを参照する参照であり、はを指すiポインタkですi。(これからは、「参照」と「ポイント」を単に「ポイント」に組み合わせます。)

この時点で、2つのcv修飾変数がjありk、は、非cv修飾オブジェクトを指します。これは§7.1で言及されています。5.1/3:

cv修飾型へのポインターまたは参照は、実際にcv修飾オブジェクトを指し示したり参照したりする必要はありませんが、そうであるかのように扱われます。参照されているオブジェクトが非constオブジェクトであり、他のアクセスパスを介して変更できる場合でも、const修飾アクセスパスを使用してオブジェクトを変更することはできません。[注:cv-qualifiersは型システムでサポートされているため、キャストせずに破壊することはできません(5.2.11)。]

これが意味することは、コンパイラーはそれを尊重しjkcv修飾されていないオブジェクトを指している場合でも、cv修飾されている必要があるということです。(つまりj = 5、合法*k = 5であるにもかかわらず、違法i = 5です。)

constここで、それらからを削除することを検討します。

const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;

これは合法です(§5.2.11を参照)が、未定義の動作ですか?いいえ。§7.1を参照してください。5.1/4:

可変(7.1.1)と宣言されたクラスメンバーを変更できることを除いて、constオブジェクトをその存続期間(3.8)中に変更しようとすると、未定義の動作が発生します。 強調鉱山。

iそれはそうではなく const、それと両方がを指していることjを忘れないでください。私たちが行ったのは、型システムに型からconst-qualifierを削除するように指示することだけです。これにより、ポイントされたオブジェクトを変更し、それらの変数を使用して変更できます。kii

これは、次のこととまったく同じです。

int& j = i; // removed const with const_cast...
int* k = &i; // ..trivially legal code

j = 5;
*k = 5;

そして、これは自明に合法です。i代わりに、これがこれであると考えます。

const int i = 3;

今の私たちのコードは何ですか?

const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;

はconst修飾オブジェクトであるため、未定義の動作につながるようになりました。型システムに、ポイントされたオブジェクトを変更できるようiに削除するように指示してから、const修飾オブジェクトを変更しました。上で引用したように、これは未定義です。const

繰り返しますが、より明白なのは:

int& j = i; // removed const with const_cast...
int* k = &i; // ...but this is not legal!

j = 5;
*k = 5;

単にこれを行うことに注意してください:

const_cast<int&>(j);
*const_cast<int*>(k);

const修飾オブジェクトは変更されていないため、完全に合法で定義されています。型システムをいじっているだけです。


今考えてみましょう:

struct foo
{
    foo() :
    me(this), self(*this), i(3)
    {}

    void bar() const
    {
        me->i = 5;
        self.i = 5;
    }

    foo* me;
    foo& self;
    int i;
};

メンバーはどうしconstますか?それらへのアクセスは、cv修飾アクセスパスbarと呼ばれるものを経由します。(これは、のタイプをからに変更することによって行われます。ここで、は関数のcv修飾子です。)thisT* constcv T const*cv

では、実行中のメンバータイプは何barですか?彼らです:

// const-pointer-to-non-const, where the pointer points cannot be changed
foo* const me;

// foo& const is ill-formed, cv-qualifiers do nothing to reference types
foo& self; 

// same as const int
int const i; 

もちろん、重要なのはポインタではなく、オブジェクトを指すconst-qualificationであるため、タイプは関係ありません。k上記const int* constの場合、後者constは関係ありません。)ここで次のことを検討します。

int main()
{
    foo f;
    f.bar(); // UB?
}

barでは、meselfは非定数を指しているfooため、int i上記と同様に、明確に定義された動作があります。私たちが持っていた:

const foo f;
f.bar(); // UB!

const intconst修飾オブジェクトを変更するので、と同じようにUBがあります。

あなたの質問では、const修飾オブジェクトがないため、未定義の動作はありません。


そして、権威に訴えるためconst_castに、非const関数でconst修飾関数をリサイクルするために使用されるScottMeyersによるトリックを考えてみましょう。

struct foo
{
    const int& bar() const
    {
        int* result = /* complicated process to get the resulting int */
        return *result; 
    }

    int& bar()
    {
        // we wouldn't like to copy-paste a complicated process, what can we do?
    }

};

彼は提案します:

int& bar(void)
{
    const foo& self = *this; // add const
    const int& result = self.bar(); // call const version
    return const_cast<int&>(result); // take off const
}

またはそれが通常どのように書かれているのか:

int& bar(void)
{
    return const_cast<int&>( // (3) remove const from result
            static_cast<const foo&>(*this) // (1) add const to this
            .bar() // (2) call const version
            ); 
}

これもまた、完全に合法であり、明確に定義されていることに注意してください。具体的には、この関数は非const修飾で呼び出される必要があるためfoo、戻り型の。からconst修飾を取り除くことは完全に安全ですint& boo() const

const_cast(誰かが最初に+呼び出しで自分自身を撃たない限り。)


要約する:

struct foo
{
    foo(void) :
    i(),
    self(*this), me(this),
    self_2(*this), me_2(this)
    {}

    const int& bar() const
    {
        return i; // always well-formed, always defined
    }

    int& bar() const
    {
        // always well-formed, always well-defined
        return const_cast<int&>(
                static_cast<const foo&>(*this).
                bar()
                );
    }

    void baz() const
    {
        // always ill-formed, i is a const int in baz
        i = 5; 

        // always ill-formed, me is a foo* const in baz
        me = 0;

        // always ill-formed, me_2 is a const foo* const in baz
        me_2 = 0; 

        // always well-formed, defined if the foo pointed to is non-const
        self.i = 5;
        me->i = 5; 

        // always ill-formed, type points to a const (though the object it 
        // points to may or may not necessarily be const-qualified)
        self_2.i = 5; 
        me_2->i = 5; 

        // always well-formed, always defined, nothing being modified
        // (note: if the result/member was not an int and was a user-defined 
        // type, if it had its copy-constructor and/or operator= parameter 
        // as T& instead of const T&, like auto_ptr for example, this would 
        // be defined if the foo self_2/me_2 points to was non-const
        int r = const_cast<foo&>(self_2).i;
        r = const_cast<foo* const>(me_2)->i;

        // always well-formed, always defined, nothing being modified.
        // (same idea behind the non-const bar, only const qualifications
        // are being changed, not any objects.)
        const_cast<foo&>(self_2);
        const_cast<foo* const>(me_2);

        // always well-formed, defined if the foo pointed to is non-const
        // (note, equivalent to using self and me)
        const_cast<foo&>(self_2).i = 5;
        const_cast<foo* const>(me_2)->i = 5;

        // always well-formed, defined if the foo pointed to is non-const
        const_cast<foo&>(*this).i = 5;
        const_cast<foo* const>(this)->i = 5;
    }

    int i;

    foo& self;
    foo* me;
    const foo& self_2;
    const foo* me_2;
};

int main()
{
    int i = 0;
    {
        // always well-formed, always defined
        int& x = i;
        int* y = &i;
        const int& z = i;
        const int* w = &i;

        // always well-formed, always defined
        // (note, same as using x and y)
        const_cast<int&>(z) = 5;
        const_cast<int*>(w) = 5;
    }

    const int j = 0;
    {
        // never well-formed, strips cv-qualifications without a cast
        int& x = j;
        int* y = &j;

        // always well-formed, always defined
        const int& z = i;
        const int* w = &i;

        // always well-formed, never defined
        // (note, same as using x and y, but those were ill-formed)
        const_cast<int&>(z) = 5;
        const_cast<int*>(w) = 5;
    }

    foo x;
    x.bar(); // calls non-const, well-formed, always defined
    x.bar() = 5; // calls non-const, which calls const, removes const from
                 // result, and modifies which is defined because the object
                 // pointed to by the returned reference is non-const,
                 // because x is non-const.

    x.baz(); // well-formed, always defined

    const foo y;
    y.bar(); // calls const, well-formed, always defined
    const_cast<foo&>(y).bar(); // calls non-const, well-formed, 
                               // always defined (nothing being modified)
    const_cast<foo&>(y).bar() = 5; // calls non-const, which calls const,
                                   // removes const from result, and
                                   // modifies which is undefined because 
                                   // the object pointed to by the returned
                                   // reference is const, because y is const.

    y.baz(); // well-formed, always undefined
}

ISO C++03規格を参照しています。

于 2010-08-14T22:16:31.760 に答える
6

IMO、あなたは技術的に間違ったことをしていません。たぶん、メンバーがポインタであるかどうかを理解する方が簡単でしょう。

class X
{
    Y* m_ptr;
    void foo() const {
        m_ptr = NULL; //illegal
        *m_ptr = 42; //legal
    }
};

constポインタをconstにしますが、pointeeではありません。

次の違いを考慮してください。

const X* ptr;
X* const ptr;  //this is what happens in const member functions

参照に関しては、とにかく再装着できないためconst、メソッドのキーワードは参照メンバーにはまったく影響しません。

あなたの例では、constオブジェクトが表示されないので、悪いことは何もしていません。C++でconstの正当性が機能する方法で奇妙な抜け穴を悪用しているだけです。

于 2010-08-14T19:55:29.777 に答える
1

それが許可されている/許可されるべき/許可されているかどうかを実際に知ることなく、私はそれに対して大いに助言したいと思います。言語には、他の開発者を混乱させる可能性が最も高いあいまいな構造を書く必要のない、達成したいことのためのメカニズムがあります。

mutableキーワードを調べてください。constそのキーワードは、クラスの知覚可能な状態に影響を与えないため、メンバーメソッド内で変更できるメンバーを宣言するために使用できます。パラメータのセットで初期化され、常に必要とは限らない複雑で高価な計算を実行するクラスについて考えてみます。

class ComplexProcessor
{
public:
   void setInputs( int a, int b );
   int getValue() const;
private:
   int complexCalculation( int a, int b );
   int result;
};

可能な実装は、結果値をメンバーとして追加し、各セットに対してそれを計算することです。

void ComplexProcessor::setInputs( int a, int b ) {
   result = complexCalculation( a, b );
}

ただし、これは、値が必要かどうかに関係なく、すべてのセットで値が計算されることを意味します。オブジェクトをブラックボックスと考えると、インターフェイスはパラメータを設定するメソッドと計算値を取得するメソッドを定義するだけです。計算が実行される瞬間は、ゲッターによって返される値が正しい限り、オブジェクトの知覚状態に実際には影響しません。したがって、クラスを変更して(出力ではなく)入力を格納し、必要な場合にのみ結果を計算できます。

class ComplexProcessor2 {
public:
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
   }
   int getValue() const {
      return complexCalculation( a_, b_ );
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
};

意味的には2番目のクラスと1番目のクラスは同等ですが、値が不要な場合は複雑な計算を実行することを避けたため、値が要求される場合がある場合にのみ利点があります。ただし、同時に、同じオブジェクトに対して値が何度も要求されると不利になります。入力が変更されていなくても、毎回複雑な計算が実行されます。

解決策は結果をキャッシュすることです。そのために、クラスに結果を出すことができます。結果が要求されたときに、すでに計算している場合はそれを取得するだけで済みますが、値がない場合は計算する必要があります。入力が変更されると、キャッシュが無効になります。これは、mutableキーワードが役立つ場合です。これは、メンバーが認識可能な状態の一部ではないため、定数メソッド内で変更できることをコンパイラーに通知します。

class ComplexProcessor3 {
public:
   ComplexProcessor3() : cached_(false) {}
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
      cached_ = false;
   }
   int getValue() const {
      if ( !cached_ ) {
         result_ = complexCalculation( a_, b_ );
         cached_ = true;
      }
      return result_;
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
   // This are not part of the perceivable state:
   mutable int result_;
   mutable bool cached_;
};

3番目の実装は、前の2つのバージョンと意味的に同等ですが、結果がすでにわかっていてキャッシュされている場合は、値を再計算する必要がありません。

このmutableキーワードは、マルチスレッドアプリケーションのように、クラスのミューテックスがしばしばマークされるなど、他の場所で必要になりますmutable。ミューテックスのロックとロック解除は、ミューテックスのミューティング操作です。その状態は明らかに変化しています。現在、異なるスレッド間で共有されているオブジェクトのgetterメソッドは、認識された状態を変更しませんが、操作がスレッドセーフである必要がある場合は、ロックを取得して解放する必要があります。

template <typename T>
class SharedValue {
public:
   void set( T v ) {
      scoped_lock lock(mutex_);
      value = v;
   }
   T get() const {
      scoped_lock lock(mutex_);
      return value;
   }
private:
   T value;
   mutable mutex mutex_;
};

valueゲッター操作は、メンバーへのシングルスレッドアクセスを保証するためにミューテックスを変更する必要がある場合でも、意味的に一定です。

于 2010-08-14T22:34:21.617 に答える
0

キーワードは、constコンパイル時のチェック中にのみ考慮されます。C ++には、ポインタ/参照で行っているメモリアクセスからクラスを保護する機能はありません。コンパイラもランタイムも、ポインタがconstをどこかで宣言したインスタンスを指しているかどうかを知ることはできません。

編集:

短い例(コンパイルされない場合があります):

// lets say foo has a member const int Foo::datalength() const {...}
// and a read only acces method const char data(int idx) const {...}

for (int i; i < foo.datalength(); ++i)
{
     foo.questionable();  // this will most likely mess up foo.datalength !!
     std::cout << foo.data(i); // HERE BE DRAGONS
}

この場合、コンパイラはfoo.datalengthがconstであると判断する可能性があり、ループ内のコードはfooを変更しないことを約束しているため、ループに入るときにデータ長を1回だけ評価する必要があります。イッピー!そして、このエラーをデバッグしようとすると、(デバッグビルドではなく)最適化を使用してコンパイルした場合にのみ発生する可能性が高く、夢中になります。

約束を守れ!または、警戒心の強い脳細胞で可変を使用してください!

于 2010-08-14T16:58:16.317 に答える
-1

循環依存に達しました。FAQ 39.11を参照してください。そうconstです、コンパイラを回避したとしても、データの変更はUBです。また、約束を守らないと、コンパイラの最適化能力が大幅に低下します(「違反」を読んでくださいconst)。

Questionable const所有者への電話で変更することがわかっているのはなぜですか?所有されているオブジェクトが所有者について知る必要があるのはなぜですか?あなたが本当にそれをする必要があるならば、それmutableから行く方法です。それがそこにあるのです-論理的恒常性(厳密なビットレベルの恒常性とは対照的)。

ドラフトn3090の私のコピーから:

9.3.2thisポインタ[class.this]

1非静的(9.3)メンバー関数の本体では、キーワードthisは右辺値、prvalue式であり、その値は関数が呼び出されるオブジェクトのアドレスです。クラスXのメンバー関数でのこのタイプはX*です。メンバー関数がconstと宣言されている場合、このタイプはconst X *であり、メンバー関数がvolatileと宣言されている場合、このタイプはvolatile X *であり、メンバー関数がconst volatileと宣言されている場合、このタイプはconstです。揮発性X*。

2 constメンバー関数では、関数が呼び出されるオブジェクトは、constアクセスパスを介してアクセスされます。したがって、constメンバー関数は、オブジェクトとその非静的データメンバーを変更してはなりません。

[強調鉱山に注意]。

UBの場合:

7.1.6.1cv-qualifiers

3 cv修飾型へのポインターまたは参照は、実際にcv修飾オブジェクトを指し示したり参照したりする必要はありませんが、そうであるかのように扱われます。参照されているオブジェクトが非constオブジェクトであり、他のアクセスパスを介して変更できる場合でも、const修飾アクセスパスを使用してオブジェクトを変更することはできません。[注:cv-qualifiersは型システムでサポートされているため、キャストせずに破壊することはできません(5.2.11)。—エンドノート]

4可変(7.1.1)と宣言されたクラスメンバーを変更できることを除いて、constオブジェクトをその存続期間(3.8)中に変更しようとすると、未定義の動作が発生します。

于 2010-08-14T16:57:29.043 に答える