MSVCの実装の例:
#define offsetof(s,m) \
(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
// ^^^^^^^^^^^
ご覧のとおり、これはnullポインターを逆参照し、通常は未定義の動作を呼び出します。これはルールの例外ですか、それとも何が起こっているのですか?
MSVCの実装の例:
#define offsetof(s,m) \
(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
// ^^^^^^^^^^^
ご覧のとおり、これはnullポインターを逆参照し、通常は未定義の動作を呼び出します。これはルールの例外ですか、それとも何が起こっているのですか?
言語標準で「未定義動作」と記載されている場合、任意のコンパイラで動作を定義できます。標準ライブラリの実装コードは通常、それに依存しています。したがって、2つの質問があります。
(1)コードはC ++標準に関してUBですか?
これは非常に難しい質問です。C++98/03標準が、一般にnullpointerを逆参照するのはUBであると規範的なテキストで正しく述べていないことは、よく知られているほとんど欠陥です。これは、 UBではないの例外によって暗示されます。typeid
はっきりと言えるのはoffsetof
、非PODタイプで使用するのはUBだということです。
(2)コードUBは、それが作成されたコンパイラーに関してですか?
いいえ、もちろん違います。
特定のコンパイラのコンパイラベンダーのコードは、そのコンパイラの任意の機能を使用できます。
乾杯&hth。、
「未定義動作」の概念は、それがマクロ、関数、またはその他のものであるかどうかに関係なく、標準ライブラリの実装には適用されません。
一般的に、標準ライブラリはC ++(またはC)言語で実装されていると見なされるべきではありません。これは、標準のヘッダーファイルにも当てはまります。標準ライブラリはその外部仕様に準拠している必要がありますが、それ以外はすべて実装の詳細であり、言語の他のすべての要件は免除されています。標準ライブラリは常に、C ++またはCによく似ているかもしれないが、それでもC++またはCではない「内部」言語で実装されていると考える必要があります。
offsetof
つまり、引用したマクロは、特に標準ライブラリで定義されているマクロである限り、未定義の動作を生成しません。ただし、コードでまったく同じことを行うと(まったく同じ方法で独自のマクロを定義するなど)、実際には未定義の動作になります。「QuodlicetJovi、nonlicetbovi」。
C標準が特定のアクションが未定義動作を呼び出すことを指定している場合、それは一般にそのようなアクションが禁止されていることを意味するのではなく、実装が結果として生じる動作を自由に指定できるかどうかを意味します。したがって、実装が定義された動作を要求する場合、実装がそれらのアクションの動作が標準が要求するものと一致することを保証できる場合に限り、実装はそのようなアクションを自由に実行できます。たとえば、次のstrcpyの実装について考えてみます。
char *strcpy(char *dest, char const *src)
{
ptrdiff_t diff = dest-src-1;
int ch;
while((ch = *src++) != 0)
src[diff] = ch;
return dest;
}
src
とが無関係のポインタである場合dest
、の計算によりdest-src
未定義動作が生成されます。char*
ただし、一部のプラットフォームでは、との間の関係は、ptrdiff_t
任意のが与えられた場合char* p1, p2
、計算p1 + (p2-p1);
は常にに等しくなりp2
ます。その保証を行うプラットフォームでは、上記の実装はstrcpy
正当です(そして、そのようなプラットフォームの中には、もっともらしい代替案よりも速いかもしれません)。ただし、他の一部のプラットフォームでは、両方の文字列が同じ割り当て済みオブジェクトの一部である場合を除いて、このような関数は常に失敗する可能性があります。
同じ原則がoffsetof
マクロにも当てはまります。offsetof
コンパイラが(実際にそのマクロを使用する以外に)同等の動作を取得する方法を提供する必要はありません。ポインタ演算のコンパイラのモデルで、nullポインタoffsetof
の演算子を使用して必要な動作を取得できる場合->
は、そのoffsetof
マクロそれを行うことができます。->
コンパイラがその型のインスタンスへの正当なポインタ以外で使用するための努力をサポートしない場合は、フィールドオフセットを計算して定義できる組み込み型を定義する必要があるかもしれません。offsetof
それを使用するマクロ。重要なのは、標準が標準ライブラリのマクロと関数を使用して実行されるアクションの動作を定義することではなく、実装ではなく、そのようなマクロと関数の動作が要件に一致することを保証することです。
これは基本的に、これがUBであるかどうかを尋ねるのと同じです。
s* p = 0;
volatile auto& r = p->m;
明らかに、のターゲットへのメモリアクセスは生成されません。これは、のターゲットでr
あり、コンパイラが変数volatile
へのスプリアスアクセスを生成することを禁止されているためです。volatile
ただし*s
、揮発性ではないため、コンパイラがアクセスを生成する可能性があります。address-of演算子も参照型へのキャストも、標準に従って未評価のコンテキストを作成しません。
したがって、の理由はわかりませんvolatile
。また、これは標準による未定義の動作であることに同意します。もちろん、どのコンパイラも、標準が実装指定または未定義のままにする動作を定義することが許可されています。
最後に、セクションのメモに[dcl.ref]
は
特に、null参照を作成する唯一の方法は、nullポインターを逆参照して取得した「オブジェクト」にバインドすることであり、未定義の動作が発生するため、null参照は明確に定義されたプログラムに存在できません。
m
が構造内のオフセット0にs
ある場合、およびその他の特定の場合は、C++での未定義の動作ではありません。Issue 232(強調鉱山)によると:
単項*演算子は間接参照を実行します。適用される式は、オブジェクト型へのポインタ、または関数型へのポインタであり、結果は、式が指すオブジェクトまたは関数を参照する左辺値になります。 。ポインターがnullポインター値(7.11 [conv.ptr])であるか、配列オブジェクトの最後の要素の1つ先を指している場合(8.7 [expr.add])、結果は空の左辺値であり、オブジェクトまたは働き。空の左辺値は変更できません。
したがって、&((s *)0)->m
が未定義の動作であるのm
は、がオフセット0でも、配列オブジェクトの最後の要素の1つ後のアドレスに対応するオフセットでもない場合のみです。に0オフセットを追加することは、C ++ではnull
許可されていますが、Cでは許可されていないことに注意してください。
他の人が指摘しているように、コンパイラは未定義の動作を作成しないことが許可されており(そして非常に可能性が高い)、特定のコンパイラの拡張仕様を利用するライブラリと一緒にパッケージ化される場合があります。
いいえ、これは未定義の動作ではありません。式は実行時に解決されます。
m
nullポインタからメンバーのアドレスを取得していることに注意してください。nullポインタを逆参照していません。