C++ では、オブジェクト スライスを派生型から基本型への変換と考える必要があります[*]。「実話にインスパイアされた」真新しいオブジェクトが作成されます。
時にはこれをやりたいと思うこともありますが、その結果は元のオブジェクトとまったく同じではありません。オブジェクトのスライスがうまくいかないのは、人々が注意を払っていないときであり、同じオブジェクトまたはそのコピーであると考えています。
通常は有利ではありません。実際、誰かが参照渡しを意図していたときに値渡しをすると、通常は誤って行われます。
非抽象基底クラスが決定的に正しいことである例を考え出すのは非常に難しいため (特に C++ では)、スライシングが決定的に正しい方法である場合の例を考え出すことは非常に困難です。これは重要な設計ポイントであり、簡単に見過ごすことはできません。意図的または偶然にオブジェクトをスライスしていることに気付いた場合は、オブジェクト階層が最初から間違っている可能性が非常に高くなります。基本クラスを基本クラスとして使用しないでください。または、少なくとも 1 つの純粋な仮想関数を持ち、スライス可能または値渡し可能にしないでください。
したがって、オブジェクトがその基本クラスのオブジェクトに変換される例を挙げた場合、「ちょっと待ってください。そもそも具体的なクラスから継承して何をしているのですか?」という反論を正しく引き起こすでしょう。スライスが偶発的なものである場合はおそらくバグであり、意図的なものである場合はおそらく「コードの匂い」です。
しかし、答えは「はい、わかりました。これは物事が実際にどのように構造化されているべきではありませんが、そのように構造化されていることを考えると、派生クラスから基本クラスに変換する必要があり、定義上、それはスライスです。 "。その精神で、ここに例があります:
struct Soldier {
string name;
string rank;
string serialNumber;
};
struct ActiveSoldier : Soldier {
string currentUnit;
ActiveSoldier *commandingOfficer; // the design errors multiply!
int yearsService;
};
template <typename InputIterator>
void takePrisoners(InputIterator first, InputIterator last) {
while (first != last) {
Soldier s(*first);
// do some stuff with name, rank and serialNumber
++first;
}
}
ここで、関数テンプレートの要件はtakePrisoners、そのパラメーターが Soldier に変換可能な型のイテレーターであることです。派生クラスである必要はなく、メンバーの「名前」などに直接アクセスしないため、takePrisoners(a) Soldier で動作する必要がある、および ( b) それが動作する他の型を書くことが可能であるべきです。
ActiveSoldier は、そのような別のタイプの 1 つです。そのクラスの作成者だけがよく知っている理由により、オーバーロードされた変換演算子を提供するのではなく、Soldier から公に継承することを選択しました。それが良いアイデアかどうかは議論できますが、それで行き詰まっているとしましょう。派生クラスなので、Soldier に変換可能です。その変換はスライスと呼ばれます。したがって、ActiveSoldiers のベクトルに対しておよびイテレータをtakePrisoners渡すと、それらがスライスされます。begin()end()
おそらく、OutputIterator の同様の例を考え出すことができます。この場合、受信者は、配信されるオブジェクトの基本クラス部分のみを気にし、イテレーターに書き込まれるときにそれらをスライスできます。
これが「コードのにおい」である理由は、(a) ActiveSoldier を書き直し、(b) Soldier をメンバー アクセスの代わりに関数を使用してアクセスできるように変更することを検討する必要があるためです。他のタイプは独立して実装できるため、takePrisonersSoldier に変換する必要はありません。どちらもスライスの必要性をなくし、将来コードを簡単に拡張できるという潜在的な利点があります。
【※】 1枚ですので。以下の最後の 2 行は同じことを行っています。
struct A {
int value;
A(int v) : value(v) {}
};
struct B : A {
int quantity;
B(int v, int q) : A(v), quantity(q) {}
};
int main() {
int i = 12; // an integer
B b(12, 3); // an instance of B
A a1 = b; // (1) convert B to A, also known as "slicing"
A a2 = i; // (2) convert int to A, not known as "slicing"
}
唯一の違いは、(1) A のコピー コンストラクター (コードが提供していなくてもコンパイラが提供するもの) を呼び出すのに対し、(2) A の int コンストラクターを呼び出すことです。
他の誰かが言ったように、Java はオブジェクトのスライスを行いません。提供したコードが Java に変換された場合、オブジェクトのスライスは発生しません。Java 変数はオブジェクトではなく参照であるため、事後条件はa = b、変数 "a" が変数 "b" と同じオブジェクトを参照することだけです。一方の参照による変更は、もう一方の参照を介して確認できます。それらは、ポリモーフィズムの一部である別の型によってそれを参照するだけです。これの典型的なアナロジーは、ある人を「私の兄弟」[**]と考えるかもしれず、他の誰かが同じ人を「私の牧師」と考えるかもしれないということです. 同じオブジェクト、異なるインターフェース。
ポインターまたは参照を使用して、C++ で Java のような効果を得ることができます。
B b(24,7);
A *a3 = &b; // No slicing - a3 is a pointer to the object b
A &a4 = b; // No slicing - a4 is a reference to (pseudonym for) the object b
[**] 実は、私の兄は牧師ではありません。