次の関数テンプレートがあります。
template <class MostDerived, class HeldAs>
HeldAs* duplicate(MostDerived *original, HeldAs *held)
{
// error checking omitted for brevity
MostDerived *copy = new MostDerived(*original);
std::uintptr_t distance = reinterpret_cast<std::uintptr_t>(held) - reinterpret_cast<std::uintptr_t>(original);
HeldAs *copyHeld = reinterpret_cast<HeldAs*>(reinterpret_cast<std::uintptr_t>(copy) + distance);
return copyHeld;
}
目的は、特定のタイプのオブジェクトを複製し、入力と同じサブオブジェクトによって「保持」されたオブジェクトを返すことです。原則として、HeldAs
は のあいまいまたはアクセスできない基本クラスになる可能性があるMostDerived
ため、ここではキャストは役に立たないことに注意してください。
これは私のコードですが、私の制御外の型で使用できます (つまり、MostDerived
またはを変更することはできませんHeldAs
)。関数には次の前提条件があります。
*original
動的タイプですMostDerived
HeldAs
MostDerived
または直接的または間接的な基底クラス(MostDerived
cv 修飾を無視)*held
*original
またはその基本クラスのサブオブジェクトの 1 つを参照します。
前提条件が満たされているとしましょう。duplicate
そのような場合の動作を定義していますか?
C++11 [expr.reinterpret.cast] は言います (太字は私のものです):
4 ポインターは、それを保持するのに十分な大きさの任意の整数型に明示的に変換できます。マッピング関数は実装定義です。[注:基礎となるマシンのアドレッシング構造を知っている人にとっては驚くべきことではありません。—終わりのメモ] ...
5 整数型または列挙型の値は、明示的にポインターに変換できます。十分なサイズの整数に変換され (実装に存在する場合)、同じポインター型に戻されるポインターは、元の値を持ちます。それ以外の場合、ポインターと整数の間のマッピングは実装定義です。[注: 3.7.4.3 で説明されている場合を除き、このような変換の結果は、安全に導出されたポインター値にはなりません。—終わりのメモ]
OK、私のコンパイラが GCC (または実装定義の動作の GCC の定義を使用するため、Clang) であるとしましょう。C ++実装定義の動作に関するGCCドキュメントの第5章を引用:
... いくつかの選択肢は、C 言語の対応するドキュメントに記載されています。C の実装を参照してください。...
4.7 章(C 実装、配列、およびポインター) に進みます。
ポインターを整数に、またはその逆に変換した結果 (C90 6.3.4、C99 および C11 6.3.2.3)。
ポインターから整数へのキャストは、ポインター表現が整数型よりも大きい場合は最上位ビットを破棄し、ポインター表現が整数型よりも小さい場合は符号拡張し、そうでない場合はビットを変更しません。
整数からポインタへのキャストは、ポインタ表現が整数型よりも小さい場合は最上位ビットを破棄し、ポインタ表現が整数型よりも大きい場合は整数型の符号に従って拡張し、それ以外の場合はビットを変更しません。
ここまでは順調ですね。whichを使用しstd::uintptr_t
ているため、どのポインターに対しても十分な大きさであることが保証されており、同じ型を扱っているため、 asが指していcopyHeld
たのと同じHeldAs
サブオブジェクトを指す必要があるように思われます。*copy
held
*original
残念ながら、GCC ドキュメントにはもう 1 つの段落があります。
ポインターから整数にキャストし、再びキャストする場合、結果のポインターは元のポインターと同じオブジェクトを参照する必要があります。そうでない場合、動作は未定義です。つまり、C99 および C11 6.5.6/8 で禁止されているように、ポインター演算の未定義の動作を回避するために整数演算を使用することはできません。
ワム。そのため、最初の 2 つの段落のルールに従っての値copyHeld
が計算されたとしても、3 番目の段落ではこれが未定義動作領域に送信されているようです。
基本的に 3 つの質問があります。
私の読みは正しく、
duplicate
未定義の動作はありますか?これはどのような未定義の動作ですか? 「正式には定義されていませんが、とにかくやりたいことをします」種類、または「ランダムなクラッシュおよび/または自発的な焼身自殺を期待する」種類ですか?
本当に未定義の場合、明確に定義された (おそらくコンパイラに依存する) 方法でそのようなことを行う方法はありますか?
私の質問は、コンパイラに関する限り GCC (および Clang) の動作に限定されていますが、一般的なデスクトップからエキゾチックなものまで、あらゆる種類のハードウェア プラットフォームを考慮した回答を歓迎します。