私は、データメンバー構文を使用してゼロコストプロパティへのこのアプローチを(再?)発明しました。これは、ユーザーが次のように記述できることを意味します。
some_struct.some_member = var;
var = some_struct.some_member;
これらのメンバーアクセスは、オーバーヘッドがゼロのメンバー関数にリダイレクトされます。
最初のテストでは、このアプローチが実際に機能することが示されていますが、未定義の動作がないことは確かではありません。アプローチを説明する簡略化されたコードは次のとおりです。
template <class Owner, class Type, Type& (Owner::*accessor)()>
struct property {
operator Type&() {
Owner* optr = reinterpret_cast<Owner*>(this);
return (optr->*accessor)();
}
Type& operator= (const Type& t) {
Owner* optr = reinterpret_cast<Owner*>(this);
return (optr->*accessor)() = t;
}
};
union Point
{
int& get_x() { return xy[0]; }
int& get_y() { return xy[1]; }
std::array<int, 2> xy;
property<Point, int, &Point::get_x> x;
property<Point, int, &Point::get_y> y;
};
テストドライバーは、このアプローチが機能し、実際にゼロコストであることを示しています(プロパティは追加のメモリを占有しません)。
int main()
{
Point m;
m.x = 42;
m.y = -1;
std::cout << m.xy[0] << " " << m.xy[1] << "\n";
std::cout << sizeof(m) << " " << sizeof(m.x) << "\n";
}
実際のコードはもう少し複雑ですが、アプローチの要点はここにあります。これは、実際のデータ(この例ではxy
)と空のプロパティオブジェクトの和集合を使用することに基づいています。 (これが機能するには、実際のデータが標準のレイアウトクラスである必要があります)。
ユニオンが必要なのは、そうでなければ、プロパティが空であるにもかかわらず、プロパティが不必要にメモリを占有するためです。
ここにUBがないと思うのはなぜですか?この標準では、標準レイアウトのユニオンメンバーの共通の初期シーケンスにアクセスできます。ここでは、共通の初期シーケンスは空です。 x
およびy
のデータメンバーは、データメンバーがないため、まったくアクセスされません。私が規格を読んだことは、これが許可されていることを示しています。 reinterpret_cast
ユニオンメンバーをそれを含むユニオンにキャストしているので、OKであるはずです。これらはポインター相互変換可能です。
これは確かに標準で許可されていますか、それともここにいくつかのUBがありませんか?
TL; DRこれはUBです。
同様に、オブジェクトの存続期間が開始する前であるが、オブジェクトが占有するストレージが割り当てられた後、またはオブジェクトの存続期間が終了した後、オブジェクトが占有するストレージが再利用または解放される前に、元のオブジェクトを使用できますが、その方法は限られています。構築中または破壊中のオブジェクトについては、[class.cdtor]を参照してください。それ以外の場合、そのようなglvalueは割り当てられたストレージを参照し、その値に依存しないglvalueのプロパティの使用は明確に定義されています。次の場合、プログラムの動作は未定義です。[...]
- glvalueは、オブジェクトの非静的メンバー関数を呼び出すために使用されます。または
definition までに、ユニオンの非アクティブなメンバーはその存続期間内にありません。
考えられる回避策は、C++ 20を使用することです [[no_unique_address]]
struct Point
{
int& get_x() { return xy[0]; }
int& get_y() { return xy[1]; }
[[no_unique_address]] property<Point, int, &Point::get_x> x;
[[no_unique_address]] property<Point, int, &Point::get_y> y;
std::array<int, 2> xy;
};
static_assert(offsetof(Point, x) == 0 && offsetof(Point, y) == 0);
common-initial-sequenceルールがユニオンについて述べていること :
構造体タイプ
T1
のアクティブメンバーを持つ標準レイアウトユニオンでは、構造体タイプT2
の別のユニオンメンバーの非静的データメンバーm
を読み取ることが許可されます。 T1とT2の共通の初期シーケンスの一部です。動作は、T1の対応するメンバーが指名されたかのようです。
あなたのコードは適格ではありません。どうして?あなたは「別の組合員」から読んでいないからです。あなたはm.x = 42;
をやっています。それは読んでいません。これは、別のユニオンメンバーのメンバー関数を呼び出しています。
したがって、一般的な初期シーケンスルールの対象にはなりません。そして、あなたを保護するためのcommon-initial-sequenceルールがなければ、ユニオンの非アクティブなメンバーにアクセスすることはUBです。