37

オブジェクトの std::vector と 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代入演算子が必要ですが、回答の1つから次のコードのように使用したくありません。

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

私の解決策は

// Custom-defined assignment operator
A& operator=(const A& right)  
{  
    if (this == &right) return *this;  

    // manually call the destructor of the old left-side object
    // (`this`) in the assignment operation to clean it up
    this->~A(); 
    // use "placement new" syntax to copy-construct a new `A` 
    // object from `right` into left (at address `this`)
    new (this) A(right); 
    return *this;  
}  

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

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

4

8 に答える 8

43

あなたのコードは未定義の振る舞いを引き起こします。

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

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

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

..。

—元のオブジェクトの型はconst-qualifiedではなく、クラス型の場合、型がconst-qualifiedまたは参照型である非静的データメンバーを含まない場合、

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

オブジェクトはクラスタイプであり、タイプがconst-qualifiedである非静的データメンバーが含まれています。したがって、代入演算子が実行された後、古いオブジェクトを参照するポインター、参照、および名前は、新しいオブジェクトを参照し、それを操作するために使用できることが保証されません。

何がうまくいかないかの具体的な例として、次のことを考慮してください。

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

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

于 2010-11-09T17:31:38.603 に答える
25

最初に: データ メンバーを作成すると、このデータ メンバーが変更さconstれないことをコンパイラと全世界に伝えます。もちろん、それに代入することはできません。また、どんなに巧妙なトリックであっても、コンパイラーを騙して、代入するコードを受け入れさせてはなりません。データ メンバーまたはすべてのデータ メンバーに代入する代入演算子を 使用できます。両方を持つことはできません。
const

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

ただし、技術的なことはさておき、コードが例のように単純である限り、ほぼすべてのプラットフォームで「ソリューション」を回避できることを認めます。それでも、これは良い解決策にはなりません。実際、 IME コードはそれほど単純ではないため、これは受け入れられる解決策でさえないと私は主張します。何年にもわたって拡張、変更、変異、ねじれが発生し、静かに失敗し、問題を見つけるために 36 時間のデバッグの気が遠くなるようなシフトが必要になります。私はあなたのことは知りませんが、このようなコードが 36 時間のデバッグの楽しみの原因になっているのを見つけたときはいつでも、私にこんなことをした惨めな愚か者の首を絞めたいと思います。

Herb Sutter は、彼のGotW # 23、このアイデアを 1 つずつ分析し、最終的に次のように結論付けてます明示的なデストラクタの後に配置 new を使用することにより、コピーの構築に関してコピーの割り当てを実装するのですが、このトリックはニュースグループで3か月ごとに発生します.

于 2010-11-09T16:53:47.633 に答える
10

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

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

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

const キャストは、常に未定義の動作を導入するとは限りません。しかし、あなたは間違いなくそうしました。T が POD クラスであることを確実に知っていない限り、すべてのデストラクタを呼び出さないことは定義されていません。さらに、さまざまな形式の継承に関連する時間未定義の動作があります。

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

于 2010-11-09T16:50:39.473 に答える
2

不変の (しかし割り当て可能な) メンバーが確実に必要な場合は、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';
}
于 2010-11-09T17:56:13.540 に答える
2

新しい C++ 標準ドラフト バージョン N4861 によると、未定義の動作ではなくなったようです(リンク) :

オブジェクトの有効期間が終了した後、オブジェクトが占有していたストレージが再利用または解放される前に、元のオブジェクトが占有していたストレージの場所に新しいオブジェクトが作成された場合、元のオブジェクトを指すポインタ、その参照または、元のオブジェクトの名前が自動的に新しいオブジェクトを参照し、新しいオブジェクトの有効期間が開始されると、元のオブジェクトが透過的に置換可能である場合、新しいオブジェクトを操作するために使用できます (を参照)。以下) 新しいオブジェクトによって。次の場合、オブジェクト o1 はオブジェクト o2 によって透過的に置き換え可能です。

  • o2 が占有するストレージは、o1 が占有するストレージを正確にオーバーレイします。
  • o1 と o2 が同じ型 (最上位の cv 修飾子を無視) であり、かつ
  • o1 は完全な const オブジェクトではなく、
  • o1 も o2 も重複する可能性のあるサブオブジェクト ([intro.object]) ではなく、
  • o1 と o2 が両方とも完全なオブジェクトであるか、o1 と o2 がそれぞれオブジェクト p1 と p2 の直接のサブオブジェクトであり、p1 が p2 によって透過的に置き換え可能です。

ここでは、const に関して「o1 は完全な const オブジェクトではありません」のみを見つけることができます。これは、この場合に当てはまります。もちろん、他のすべての条件にも違反していないことを確認する必要があります。

于 2020-08-19T14:22:20.073 に答える
1

他の (非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ベルを鳴らして、何かがおかしいことを知らせる必要があります。 主に正しくない API をconst_cast回避するように設計されており、ここではそうではないようです。const

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

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;
}
于 2010-11-09T17:04:38.557 に答える
0

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

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

特に...

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

于 2010-11-09T17:04:07.373 に答える