誰かが IRC でスライシングの問題として言及しました。
18 に答える
「スライス」とは、派生クラスのオブジェクトを基本クラスのインスタンスに割り当てることで、情報の一部が失われ、一部が「スライス」されることです。
例えば、
class A {
int foo;
};
class B : public A {
int bar;
};
したがって、 型のオブジェクトにB
は と の 2 つのデータ メンバーがfoo
ありbar
ます。
次に、これを書くとしたら:
B b;
A a = b;
次に、b
メンバーについての情報bar
が で失われa
ます。
ここでのほとんどの回答は、スライスの実際の問題が何であるかを説明できません。彼らはスライスの無害なケースのみを説明し、危険なケースは説明しません. 他の回答と同様に、と の 2 つのクラスA
を扱っていると仮定します。B
B
A
この場合、C++ ではB
to A
の代入演算子 (およびコピー コンストラクター) のインスタンスを渡すことができます。これが機能するのは、 のインスタンスをB
に変換できるためconst A&
です。これは、代入演算子とコピー コンストラクターが引数を期待するものです。
良性のケース
B b;
A a = b;
そこでは悪いことは何も起こりません - あなたA
は のコピーであるのインスタンスを要求しましたが、それはB
まさにあなたが得たものです。確かに、のメンバーのa
一部は含まれませんがb
、どのようにすればよいでしょうか? これはA
であり、ではないため、これらのメンバーについて聞いたB
こともありません。
裏切りのケース
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
b2
後はコピーだと思うかもしれませんb1
。しかし、残念ながらそうではありません。それを調べると、それが(から継承するチャンク) のいくつかのチャンクと(のみを含むチャンク) のb2
いくつかのチャンクから作られた、フランケンシュタインの生き物であることがわかります。痛い!b1
B
A
b2
B
どうしたの?C++ はデフォルトで代入演算子を として扱いませんvirtual
。したがって、この行は の代入演算子ではなく、a_ref = b1
の代入演算子を呼び出します。これは、非仮想関数の場合、実際の(形式的にはdynamic ) タイプ (のインスタンスを参照するため、となる)とは対照的に、宣言された(形式的にはstatic ) 型 (これは. ここで、の代入演算子は で宣言されたメンバーのみを明らかに知っているため、それらのみをコピーし、追加されたメンバーは変更されません。A
B
A&
B
a_ref
B
A
A
B
解決策
通常、オブジェクトの一部にのみ代入することはあまり意味がありませんが、残念ながら、C++ にはこれを禁止する組み込みの方法がありません。ただし、自分でロールすることはできます。最初のステップは、代入演算子をvirtualにすることです。これにより、宣言された型ではなく、常に実際の型の代入演算子が呼び出されることが保証されます。2 番目のステップは、割り当てられたオブジェクトに互換性のある型があることを確認するために使用することです。3 番目のステップは、(保護された!) member で実際の代入を行うことです。dynamic_cast
assign()
B
assign()
A
assign()
A
class A {
public:
virtual A& operator= (const A& a) {
assign(a);
return *this;
}
protected:
void assign(const A& a) {
// copy members of A from a to this
}
};
class B : public A {
public:
virtual B& operator= (const A& a) {
if (const B* b = dynamic_cast<const B*>(&a))
assign(*b);
else
throw bad_assignment();
return *this;
}
protected:
void assign(const B& b) {
A::assign(b); // Let A's assign() copy members of A from b to this
// copy members of B from b to this
}
};
のインスタンスを返すことがわかっているため、純粋な便宜上、は戻り値B
のoperator=
型を共変にオーバーライドすることに注意してください。B
基本クラスA
と派生クラスがある場合B
、次のことができます。
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
ここで、メソッドwantAnA
には のコピーが必要ですderived
。ただし、オブジェクトderived
を完全にコピーすることはできません。これは、クラスB
がその基本クラスにない追加のメンバー変数を作成する可能性があるためA
です。
したがって、 を呼び出すためにwantAnA
、コンパイラは派生クラスのすべての追加メンバーを「スライス」します。その結果、作成したくないオブジェクトになる可能性があります。
- 不完全かもしれませんが、
A
-objectのように動作します (クラスの特別な動作はすべてB
失われます)。
「C ++ slicing」のGoogleでの3番目の一致により、このウィキペディアの記事http://en.wikipedia.org/wiki/Object_slicingとこれが得られます(加熱されていますが、最初のいくつかの投稿で問題が定義されています): http: //bytes.com/フォーラム/thread163565.html
つまり、サブクラスのオブジェクトをスーパークラスに割り当てるときです。スーパークラスはサブクラスの追加情報について何も知らず、それを格納する余地がないため、追加情報は「切り取られ」ます。
これらのリンクから「適切な回答」を得るのに十分な情報が得られない場合は、質問を編集して、お探しの情報をお知らせください。
スライシングの問題は、メモリの破損につながる可能性があるため深刻であり、プログラムがスライシングに悩まされていないことを保証することは非常に困難です。言語外で設計するには、継承をサポートするクラスは参照のみでアクセスできるようにする必要があります (値ではアクセスできません)。D プログラミング言語には、このプロパティがあります。
クラス A と、A から派生したクラス B について考えてみます。A 部分にポインタ p があり、p が B の追加データを指す B インスタンスがある場合、メモリ破損が発生する可能性があります。次に、追加のデータが切り捨てられると、p はガベージを指します。
では、なぜ導出された情報を失うことが悪いのでしょうか? ...派生クラスの作成者が表現を変更して、余分な情報を切り取るとオブジェクトによって表される値が変わる可能性があるためです。これは、派生クラスが特定の操作に対してより効率的な表現をキャッシュするために使用された場合に発生する可能性がありますが、基本表現に戻すにはコストがかかります。
また、スライスを回避するために何をすべきかについて誰かが言及する必要があると考えました... C++ コーディング標準、101 ルールのガイドライン、およびベスト プラクティスのコピーを入手してください。スライスの扱いは#54です。
問題に完全に対処するためのやや洗練されたパターンを提案します: 保護されたコピー コンストラクター、保護された純粋な仮想 DoClone、および (さらに) 派生クラスが DoClone を正しく実装できなかった場合に通知するアサートを含むパブリック Clone を使用します。(Clone メソッドは、ポリモーフィック オブジェクトの適切なディープ コピーを作成します。)
また、必要に応じて明示的なスライスを可能にするベースの明示的なコピー コンストラクターをマークすることもできます。
C++ のスライシングの問題は、そのオブジェクトの値のセマンティクスから発生します。これは、主に C 構造体との互換性のために残っています。オブジェクトを処理する他のほとんどの言語に見られる「通常の」オブジェクトの動作を実現するには、明示的な参照またはポインター構文を使用する必要があります。つまり、オブジェクトは常に参照によって渡されます。
簡単な答えは、派生オブジェクトをベースオブジェクトに値で割り当てることによってオブジェクトをスライスすることです。つまり、残りのオブジェクトは派生オブジェクトの一部にすぎません。値のセマンティクスを保持するために、スライスは合理的な動作であり、他のほとんどの言語には存在しない比較的まれな用途があります。これを C++ の機能と考える人もいれば、C++ の癖/欠点の 1 つと考える人もいます。
OK、オブジェクトのスライスについて説明している投稿をたくさん読んだ後、試してみますが、それがどのように問題になるかについては説明しません。
メモリの破損につながる可能性のある悪質なシナリオは次のとおりです。
- Class は、ポリモーフィックな基本クラスで (偶然、おそらくコンパイラによって生成された) 代入を提供します。
- クライアントは、派生クラスのインスタンスをコピーしてスライスします。
- クライアントは、スライスされた状態にアクセスする仮想メンバー関数を呼び出します。
スライスとは、サブクラスのオブジェクトが値によって、または基本クラスのオブジェクトを期待する関数から渡されるか返されるときに、サブクラスによって追加されたデータが破棄されることを意味します。
説明: 次のクラス宣言を検討してください。
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
基本クラスのコピー関数は派生について何も知らないため、派生の基本部分のみがコピーされます。これは一般的にスライスと呼ばれます。
class A
{
int x;
};
class B
{
B( ) : x(1), c('a') { }
int x;
char c;
};
int main( )
{
A a;
B b;
a = b; // b.c == 'a' is "sliced" off
return 0;
}
派生クラス オブジェクトが基本クラス オブジェクトに割り当てられると、派生クラス オブジェクトの追加の属性が基本クラス オブジェクトから切り離されます (破棄されます)。
class Base {
int x;
};
class Derived : public Base {
int z;
};
int main()
{
Derived d;
Base b = d; // Object Slicing, z of d is sliced off
}