web-dev-qa-db-ja.com

constメンバーと代入演算子。未定義の動作を回避する方法は?

私は 回答済みstd :: vector of objects and const-correctness についての質問で、未定義の動作に関するコメントを受け取りました。同意しないので質問があります。

Constメンバーを持つクラスを考えてみましょう:

class A { 
public: 
    const int c; // must not be modified! 
    A(int c) : c(c) {} 
    A(const A& copy) : c(copy.c) { }     
    // No assignment operator
}; 

代入演算子が必要ですが、次のコードのようにconst_castを使用したくありません。

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is undefined behavior
    return *this; 
} 

私の解決策は

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}  

未定義の動作(UB)はありますか?

UBなしのソリューションは何でしょうか?

34
Alexey Malistov

コードにより未定義の動作が発生します。

単に「Aが基本クラスとして使用されている場合は、定義されていません。これ、またはその他」。実際には常に未定義です。 return *thisはすでにUBです。これは、thisが新しいオブジェクトを参照することが保証されていないためです。

具体的には、3.8/7を検討してください。

オブジェクトの存続期間が終了した後、オブジェクトが占有していたストレージが再利用または解放される前に、元のオブジェクトが占有していたストレージ位置に新しいオブジェクトが作成された場合、元のオブジェクトを指すポインター、その参照元のオブジェクトを参照した場合、または元のオブジェクトの名前が自動的に新しいオブジェクトを参照し、新しいオブジェクトの存続期間が開始すると、次の場合に新しいオブジェクトを操作できます。

...

—元のオブジェクトの型がconst修飾されておらず、クラス型の場合、型がconst修飾または参照型の非静的データメンバーを含んでいない。

ここで、「オブジェクトの存続期間が終了してから、オブジェクトが占有していたストレージが再利用または解放されるまでの間に、元のオブジェクトが占有していたストレージの場所に新しいオブジェクトが作成されます」とまったく同じです。

あなたのオブジェクトはクラス型であり、doesには型がconst修飾された非静的データメンバーが含まれています。したがって、割り当て演算子が実行された後、古いオブジェクトを参照するポインター、参照、および名前は、新しいオブジェクトを参照し、使用可能であることが保証されませんそれを操作する。

問題が発生する可能性のある具体的な例として、次のことを考慮してください。

A x(1);
B y(2);
std::cout << x.c << "\n";
x = y;
std::cout << x.c << "\n";

この出力を期待していますか?

1
2

違う!その出力を得る可能性はありますが、constメンバーが3.8/7で述べられたルールの例外である理由は、コンパイラーがx.cを主張するconstオブジェクトとして扱うことができるようにするためです。つまり、コンパイラはこのコードを次のように扱うことができます。

A x(1);
B y(2);
int tmp = x.c
std::cout << tmp << "\n";
x = y;
std::cout << tmp << "\n";

(非公式)constオブジェクトは値を変更しないため。 constオブジェクトを含むコードを最適化する場合のこの保証の潜在的な価値は明らかです。 UBを呼び出さずにx.cを変更する方法があるため、この保証は削除する必要があります。したがって、標準的な作家がエラーなしで仕事をしている限り、あなたが望むことをする方法はありません。

[*]実際、プレースメントの新しい引数としてthisを使用することに疑問があります。おそらく、最初にvoid*にコピーして、それを使用する必要がありました。しかし、それが特にUBであるかどうかは気になりません。それは、関数全体を保存しないからです。

40
Steve Jessop

まず、データメンバーconstを作成すると、コンパイラと全世界にこのデータメンバーは変更されない。もちろんあなたはそれに割り当てることができませんそしてあなたは確かにmustトリックではありませんどんなに巧妙なトリックであっても、コンパイラはそれを行うコードを受け入れます。
constデータメンバーorを使用して、すべてのデータメンバーに割り当て演算子を割り当てることができます。 両方を指定することはできません。

問題の「解決策」について:
そのオブジェクトに対して呼び出されたメンバー関数内のオブジェクトでデストラクタを呼び出すが呼び出されると思います[〜#〜] ub [〜#〜]すぐに。 初期化されていない生データでコンストラクターを呼び出して、メンバーが生データで呼び出された場所に存在するオブジェクトに対して呼び出されたメンバー関数内からオブジェクトを作成する ...またvery[〜#〜] ub [〜#〜]のように聞こえます。 (地獄、これを綴るだけで私の足の爪は丸くなります。)そして、いいえ、そのための標準の章と節はありません。標準を読むのは嫌いです。そのメーターには我慢できないと思います。

ただし、技術的なことはさておき、ほぼすべてのプラットフォームで「ソリューション」を使いこなせるようになることを認めますコードが例のように単純である限り。それでも、これがgoodソリューションになるわけではありません。実際、IMEコードはこれほど単純なままではないため、acceptableソリューションでさえない、と私は主張します。何年にもわたって、拡張、変更、変異、およびねじれが発生し、その後、静かに失敗し、問題を見つけるためにデバッグの面倒な36時間のシフトが必要になります。私はあなたのことは知りませんが、36時間のデバッグの楽しみの原因となっているこのようなコードを見つけたときはいつでも、私にこれを行った惨めなばかばかしいウィットを絞殺したいと思います。

GotW#2 のハーブサッターは、このアイデアを少しずつ分析し、最後に結論を出しますそれは落とし穴がいっぱいである、それはしばしば間違っている、そしてそれ派生クラスの作者にとって人生は生き地獄になります ... ニュースグループで3か月ごとにこのトリックが発生する場合でも、明示的なデストラクタを使用してコピー作成の観点からコピー割り当てを実装するトリックを使用し、その後にnewを配置しないでください(鉱山を強調) 。

23
sbi

Aにconstメンバーがある場合、Aにどのように割り当てることができますか?あなたは根本的に不可能である何かを達成しようとしています。あなたのソリューションは、必ずしもUBである必要はありませんが、あなたのソリューションは間違いなくそうです。

単純な事実は、constメンバーを変更することです。メンバーをunconstするか、代入演算子を破棄する必要があります。あなたの問題に対する解決策はありません-それは完全に矛盾しています。

より明確にするために編集します。

Constキャストは常に未定義の動作をもたらすとは限りません。しかし、あなたは確かにそうしました。他のものとは別に、すべてのデストラクタを呼び出すことは定義されていません。また、TがPODクラスであることを確実に知らない限り、デストラクタを配置する前に、正しいデストラクタを呼び出すことさえありませんでした。さらに、さまざまな形式の継承に関連する、未定義の動作が一時的に存在します。

未定義の動作を呼び出しますが、constオブジェクトに割り当てようとしないことでこれを回避できます。

9
Puppy

不変の(ただし割り当て可能な)メンバーが確実に必要な場合は、UBなしで次のようにレイアウトできます。

#include <iostream>

class ConstC
{
    int c;
protected:
    ConstC(int n): c(n) {}
    int get() const { return c; }
};

class A: private ConstC
{
public:
    A(int n): ConstC(n) {}
    friend std::ostream& operator<< (std::ostream& os, const A& a)
    {
        return os << a.get();
    }
};

int main()
{
    A first(10);
    A second(20);
    std::cout << first << ' ' << second << '\n';
    first = second;
    std::cout << first << ' ' << second << '\n';
}
2
UncleBens

このリンクを読んでください:

http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=368

特に...

このトリックにより、コードの重複が防止されます。ただし、重大な欠陥がいくつかあります。機能するためには、Cのデストラクタが削除したすべてのポインタにNULLifyを割り当てる必要があります。これは、後続のコピーコンストラクタの呼び出しで、char配列に新しい値を再割り当てするときに同じポインタが再度削除される可能性があるためです。

0
Roddy

他の(const以外の)メンバーがない場合、これは、未定義の動作かどうかに関係なく、まったく意味がありません。

_A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is UB
    return *this; 
}
_

申し訳ありませんが、cが_static const_インスタンスではないか、コピー割り当て演算子を呼び出せなかったため、ここで発生する未定義の動作はありません。ただし、_const_cast_はベルを鳴らして、何か問題があることを通知します。 _const_cast_は主にconst- correct APIを回避するように設計されており、ここではそうではないようです。

また、次のスニペットでは:

_A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}
_

あなたには2つの主要なリスクがあり、その1つ目はすでに指摘されています。

  1. 両方Aの派生クラスのインスタンス仮想デストラクタが存在する場合、これは元のインスタンスの部分的な再構築。
  2. new(this) A(right);のコンストラクター呼び出しが例外をスローすると、オブジェクトは2回破棄されます。この特定のケースでは、それは問題にはなりませんが、大幅なクリーンアップが行われた場合は、後悔することになります。

編集:オブジェクトの「状態」とは見なされないこのconstメンバーがクラスにある場合(つまり、インスタンスの追跡に使用されるある種のIDであり、 _operator==_など)の場合、次のように記述します。

_A& operator=(const A& assign) 
{ 
    // Copy all but `const` member `c`.
    // ...

    return *this;
}
_
0
André Caron