トピックを示すために、Cを使用しますが、同じマクロをC++でも使用でき(struct
の有無にかかわらず)、同じ質問が発生します。
私はこのマクロを思いついた
#define STR_MEMBER(S,X) (((struct S*)NULL)->X, #X)
その目的は、struct
の既存のメンバーの文字列(const char*
)を保持することです。これにより、メンバーが存在しない場合、コンパイルは失敗します。最小限の使用例:
#include <stdio.h>
struct a
{
int value;
};
int main(void)
{
printf("a.%s member really exists\n", STR_MEMBER(a, value));
return 0;
}
value
がstruct a
のメンバーでない場合、コードはコンパイルされません。これが私が望んでいたことです。
コンマ演算子は、左のオペランドを評価してから、式の結果(存在する場合)を破棄する必要があります。そのため、通常、左のオペランドの評価に副作用がある場合は、この演算子が使用されます。
ただし、この場合、(意図された)副作用はありませんが、もちろん機能しますiffコンパイラは実際にはコードを生成しません。式を評価します。そうしないと、struct
にあるNULL
にアクセスし、セグメンテーション違反が発生します。
Gcc/g ++ 6.3および4.9.2は、-O0
を使用しても、評価に副作用がないことを常に「確認」できるかのように、その危険なコードを生成することはなかったため、スキップできます。
マクロにvolatile
を追加する(たとえば、そのメモリアドレスにアクセスすることが目的の副作用であるため)これまでのところ、セグメンテーション違反。
したがって、質問:CおよびC++言語標準には、評価に副作用がないことをコンパイラが確認できる場合に、コンパイラがコンマ演算子の左側のオペランドの実際の評価を常に回避することを保証するものはありますか?
私はマクロをそのままと、それを使用したり、より良くしたりする機会についての判断を求めていません。この質問の目的のために、マクロはifであり、未定義の動作を引き起こす場合にのみ、悪いifそして、の場合にのみ、これが副作用がない場合でもコンパイラが「評価コード」を生成することを許可されているため、リスクがあります。
私はすでに2つの明らかな修正を念頭に置いています。struct
を「具体化」することとoffsetof
を使用することです。前者には、STR_MEMBER
の最初の引数として使用する最大のstruct
と同じ大きさのアクセス可能なメモリ領域が必要です(たとえば、静的ユニオンで実行できる可能性があります…)。後者は問題なく動作するはずです。これにより、関心のないオフセットが提供され、アクセスの問題が回避されます。実際、gccであると想定しています。これは、私が使用しているコンパイラ(したがってタグ)、およびそのoffsetof
組み込みが動作すること。
offsetof
を修正すると、マクロは次のようになります。
#define STR_MEMBER(S,X) (offsetof(struct S,X), #X)
volatile struct S
の代わりにstruct S
を書き込んでも、セグメンテーション違反は発生しません。
他の可能な「修正」についての提案も歓迎します。
実際、実際の使用例は、静的ストレージstruct
のC++でした。これはC++では問題ないようですが、この質問で煮込んだコードではなく、元のコードに近いコードでCを試してみると、Cはまったく満足していないことに気付きました。
error: initializer element is not constant
Cは、コンパイル時に構造体を初期化できるようにしたいと考えていますが、C++では問題ありません。
CおよびC++言語標準には、コンパイラがコンマ演算子の左側のオペランドの実際の評価を常に回避することを保証するものはありますか?
それは反対です。標準では、左側のオペランドISが評価されることが保証されています(実際には評価されますが、例外はありません)。結果は破棄されます。
注:左辺値式の場合、「evaluate」は「保存された値にアクセスする」という意味ではありません。代わりに、指定されたメモリ位置がどこにあるかを計算することを意味します。左辺値式を含む他のコードは、メモリ位置にアクセスする場合としない場合があります。メモリ位置から読み取るプロセスは、Cでは「左辺値変換」、C++では「左辺値から右辺値への変換」として知られています。
C++では、破棄された値の式(コンマ演算子の左オペランドなど)は、それがvolatile
であり、他のいくつかの基準も満たしている場合にのみ、左辺値から右辺値への変換が実行されます(C++ 14 [expr ]/11詳細)。 C左辺値変換では、結果が使用されない式に対してdoesが発生します(C11 6.3.2.1/2)。
あなたの例では、左辺値の変換が行われるかどうかは重要ではありません。どちらの言語でも、X
がポインタであるX->Y
は、(*X).Y
として定義されます。 Cでは、*
をnullポインターに適用する行為は、すでに未定義の動作を引き起こし(C11 6.5.3/3)、C++では、.
演算子は、左のオペランドが実際に指定する場合にのみ定義されます。オブジェクト(C++ 14 [expr.ref] /4.2)。
Gcc/g ++ 6.3および4.9.2は、-O0を使用しても、評価に副作用がないことを常に「確認」できるかのように、その危険なコードを生成することはなかったため、スキップできます。
clangは、-fsanitize=undefined
オプションを渡すとエラーを発生させるコードを生成します。これはあなたの質問に答えるはずです:少なくとも1人の主要な実装の開発者は明らかにコードが未定義の振る舞いをしていると考えています。そして、それらは正しいです。
他の可能な「修正」についての提案も歓迎します。
式を評価しないことが保証されているものを探します。 offsetof
の提案は機能しますが、X
がa.b
の場合など、コードが拒否されることがあります。それを受け入れたい場合は、sizeof
を使用して、式を未評価のままにすることをお勧めします。
あなたが尋ねる、
cおよびC++言語標準には、評価に副作用がないことをコンパイラーが確認できる場合に、コンパイラーがコンマ演算子の左オペランドの実際の評価を常に回避することを保証するものはありますか?
他の人が言っているように、答えは「いいえ」です。それどころか、標準では、コンマ演算子の左側のオペランドisが評価され、結果が破棄されると無条件に規定されています。
もちろん、これは抽象マシンの実行モデルの説明です。観察可能な動作が抽象マシンの動作と同じである限り、実装は異なる動作をすることが許可されます。実際に左側の式の評価で副作用が発生しない場合は、permitで完全にスキップされますが、どちらの標準にも規定されているものはありません。 スキップする必要があります。
それを修正することに関しては、あなたは様々なオプションを持っています、それらのいくつかはあなたが名前を付けた2つの言語のどちらか一方にのみ適用されます。私はあなたのoffsetof()
の代替案を好む傾向がありますが、C++には、offsetof
を適用できないタイプがあると指摘する人もいます。一方、Cでは、標準はstructure型への適用について具体的に説明していますが、共用体型については何も述べていません。技術的に定義されていないため、共用体タイプでの動作は一貫していて自然である可能性が非常に高いです。
Cの場合のみ、複合リテラルを使用して、アプローチでの未定義の動作を回避できます。
#define HAS_MEMBER(T,X) (((T){0}).X, #X)
これは、構造体と共用体の型でも同様に機能します(ただし、このバージョンでは、タグだけでなく、完全な型名を指定する必要があります)。特定のタイプにそのようなメンバーがある場合、その動作は明確に定義されます。構造体型でも共用体型でもない場合を含め、型にそのようなメンバーがない場合、展開は言語制約に違反するため、診断を発行する必要があります。
@alainが提案したように、sizeof
を使用することもできます。これは、sizeof
式は評価されますが、itsオペランドであるためです。評価されません(ただし、Cでは、オペランドの型が可変的に変更されている場合を除きます。これは、使用には適用されません)。このバリエーションは、未定義の動作を導入することなく、CとC++の両方で機能すると思います。
#define HAS_MEMBER(T,X) (sizeof(((T *)NULL)->X), #X)
構造体と共用体の両方で機能するように、もう一度書きました。
コンマ演算子の左側のオペランドは、破棄された値の式です。
5式
11一部のコンテキストでは、式はその副作用に対してのみ表示されます。このような式は、破棄値式と呼ばれます。式が評価され、その値は破棄されます。 [...]
未評価のオペランドもあり、名前が示すように、評価されません。
8一部のコンテキストでは、未評価のオペランドが表示されます(5.2.8、5.3.3、5.3.7、7.1.6.2)。未評価のオペランドは評価されません。評価されていないオペランドは完全な式と見なされます。 [...]
ユースケースで破棄値式を使用することは未定義の動作ですが、未評価のオペランドを使用することはそうではありません。
たとえば、sizeof
を使用しても、未評価のオペランドを使用するため、UBは発生しません。
#define STR_MEMBER(S,X) (sizeof(S::X), #X)
sizeof
は、標準レイアウトではない静的メンバーおよびクラスには使用できないため、offsetof
よりもoffsetof
の方が適しています。
18言語サポートライブラリ
4マクロoffsetof(type、member-designator)は、この国際規格で制限された型引数のセットを受け入れます。 typeが標準レイアウトクラス(第9節)でない場合、結果は未定義です。 [...]静的データメンバーまたは関数メンバーであるフィールドにoffsetofマクロを適用した結果は未定義です。 [...]
as-ifルール であるため、言語は「実際の実行」について何も言う必要はありません。結局のところ、副作用なしで、式が評価されているかどうかをどのように判断できますか? (アセンブリを見たり、ブレークポイントを設定したりすることは重要ではありません。それはプログラムの実行の一部ではありません。これはすべての言語で説明されています。)
一方、nullポインターの逆参照は未定義の動作であるため、言語は何が起こるかについてまったく何も言いません。 as-ifがあなたを救うことを期待することはできません:as-ifは、実装に対する他の点ではもっともらしい制限のrelaxationであり、未定義の動作は実装に対するall制限の緩和。したがって、「これには副作用がないため、無視できます」と「これは未定義の動作であるため、鼻の悪魔」の間に「競合」はありません。彼らは同じ側にいます!