なぜこれはタイプパンニングされたポインターの警告を逆参照しているとコンパイラ固有に主張されているのですか?


38

Stack Overflow REのさまざまな 投稿 読みました:逆参照型でパンされたポインターエラーです。私の理解では、エラーは本質的に、異なる型のポインターを介してオブジェクトにアクセスする危険性についてのコンパイラー警告である(例外はに対して行われているように見えます)と理解でき、これは理解できる合理的な警告です。char*

私の質問は以下のコードに固有です:なぜポインターのアドレスvoid**をこの警告の修飾にキャストするの-Werrorですか(を介してエラーにプロモートされます)?

さらに、このコードは複数のターゲットアーキテクチャ用にコンパイルされており、そのうちの1つだけが警告/エラーを生成します。これは、正当にコンパイラのバージョン固有の欠陥であることを意味しているのでしょうか?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

上記の私の理解が正しければ、はvoid**単なるポインタであり、これは安全にキャストできるはずです。

このコンパイラ固有の警告/エラーを緩和するlvalue使用しない回避策はありますか?つまり、私はそれが問題を解決する理由と理由を理解していfreeFunc() ますが、意図した出力引数のNULLを利用したいので、このアプローチは避けたいと思います

void* tmp = f;
freeFunc( &tmp );
f = NULL;

問題のコンパイラ(1つのうちの1つ):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

不満のないコンパイラ(多くの1つ):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

更新:さらに、コンパイル時に特に警告が生成されるように見えることを発見しました-O2(まだ「問題のあるコンパイラ」のみが記載されています)



1
void**まだ、単なるポインタなので、これは安全にキャストできるはずです。」スキッ!あなたはいくつかの基本的な仮定が続いているように思えます。バイトとレバーの観点ではなく、抽象化の観点から考えるようにしてください。それは、実際にプログラミングしているものだからです
ライトネスレースin Orbit

7
接して、あなたが使っているコンパイラーは15歳と17歳です!私はどちらにも依存しません。
Tavian Barnes

4
@TavianBarnesまた、何らかの理由でGCC 3に依存する必要がある場合は、3.4.6であった寿命末期の最終バージョンを使用するのが最善だと思います。それが休む前にそのシリーズに利用可能なすべての修正を利用してみませんか?
Kaz

どのようなC ++コーディング標準がこれらすべてのスペースを規定していますか?
Peter Mortensen

回答:


33

typeの値は、type void**のオブジェクトへのポインタvoid*です。タイプのオブジェクトはタイプのオブジェクトでFoo*はありませんvoid*

間の暗黙的な変換があるタイプのFoo*とはvoid*。この変換により、値の表現が変わる場合があります。同様に、あなたは書くことができint n = 3; double x = n;、これはxvalue に設定する明確な振る舞いを持っていますが3.0double *p = (double*)&n;未定義の振る舞いを持っています(そして実際には、一般的なアーキテクチャでpは「へのポインタ」に設定しません3.0)。

オブジェクトへの異なるタイプのポインターが異なる表現を持つアーキテクチャーは、今日ではまれですが、C標準では許可されています。(まれ)古いマシンがあるワードポインタメモリとのワードのアドレスでバイトポインタをこの言葉でバイトオフセットを持つ単語一緒のアドレスです。このようなアーキテクチャではFoo*、ワードポインタとvoid*バイトポインタになります。オブジェクトのアドレスだけでなく、そのタイプ、サイズ、およびアクセス制御リストに関する情報を含むファットポインターを備えた(まれな)マシンがあります。明確な型へのポインターは、void*実行時に追加の型情報を必要とするものとは異なる表現を持つ場合があります。

このようなマシンはまれですが、C標準で許可されています。また、一部のCコンパイラは、コードを最適化するために、タイプ制限されたポインタを別個のものとして扱う許可を利用します。ポインターのエイリアシングのリスクは、コードを最適化するコンパイラーの機能に対する主要な制限であるため、コンパイラーはそのような許可を利用する傾向があります。

コンパイラは、あなたが何か間違ったことをしているとあなたに告げたり、あなたが望んでいないことを静かにしたり、あなたが望んでいたことを静かにしたりすることは自由です。未定義の動作は、これらのいずれかを許可します。

あなたはfreefuncマクロを作ることができます:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

これには、マクロの通常の制限が伴います。型の安全性の欠如は、p2回評価されます。これはp、解放されたオブジェクトへの単一のポインタである場合に、ぶら下がっているポインタを残さないという安全性を与えるだけであることに注意してください。


1
そして、それの良いがそれを知っている場合でもするFoo*void*、あなたのアーキテクチャ上の同じ表現を持っている、それはまだ未定義だタイプしゃれ彼らに。
Tavian Barnes

12

A void *は不完全な型を参照するため、C標準によって特別に扱われます。この治療法はないではないに及ぶvoid **ことがようありません具体的には、完全な型にポイントをvoid *

厳密なエイリアシングルールでは、ある型のポインターを別の型のポインターに変換し、その後そのポインターを逆参照することはできないとしています。唯一の例外は、オブジェクトの表現を読み取ることができる文字タイプに変換する場合です。

この制限を回避するには、関数の代わりに関数のようなマクロを使用します。

#define freeFunc(obj) (free(obj), (obj) = NULL)

あなたはこのように呼び出すことができます:

freeFunc(f);

ただし、上記のマクロはobj2回評価されるため、これには制限があります。GCCを使用している場合、いくつかの拡張機能、特にtypeofキーワードとステートメントの式を使用すると、これを回避できます。

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
目的の動作をより適切に実装するための+1。私が直面する唯一の問題#defineは、obj2回評価されることです。しかし、その2番目の評価を回避する良い方法はわかりません。ステートメント式(GNU拡張)でも、objその値を使用した後に割り当てる必要があるので、うまくいきません。
cmaster

2
@cmaster:ステートメント式などのGNU拡張機能を使用する場合は、2回typeof評価することを避けるために使用できます。obj#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
ruakh

@ruakhとてもかっこいい:-) dbushがそれを回答に編集して、コメントで大量削除されないようにするのは素晴らしいことです。
cmaster

9

タイプでパンされたポインターの逆参照はUBであり、何が起こるかを期待することはできません。

コンパイラが異なれば警告も異なります。このため、同じコンパイラの異なるバージョンを異なるコンパイラと見なすことができます。これは、アーキテクチャへの依存よりも、分散のより良い説明のようです。

この場合の型パンニングが良くない理由を理解するのに役立つケースは、関数がのアーキテクチャで機能しないことsizeof(Foo*) != sizeof(void*)です。これは標準で承認されていますが、これが真実である現在のバージョンは知りません。

回避策は、関数の代わりにマクロを使用することです。

freenullポインタを受け入れることに注意してください。


2
それが可能であることは魅力的ですsizeof Foo* != sizeof void*。タイプに依存する「実際の」ポインタサイズに遭遇したことはありません。そのため、長年にわたって、特定のアーキテクチャ上でポインタサイズがすべて同じであるということは自明であると考えてきました。
StoneThrow

1
@Stonethrow標準の例は、ワードアドレス指定可能なアーキテクチャでバイトをアドレス指定するために使用されるファットポインターです。しかし、現在のワードアドレス指定可能なマシンは、代替のsizeof char == sizeof wordを使用していると思います。
AProgrammer '11 / 11/19

2
sizeofの場合、型を括弧で
囲む

@StoneThrow:ポインターのサイズに関係なく、型ベースのエイリアス分析は安全ではありません。これは、ストアがオブジェクトをfloat*変更しないと想定することにより、コンパイラーが最適化するのに役立ちます。int32_tたとえば、コンパイラーは、同じメモリーを指していないと想定するint32_t*必要はありませんint32_t *restrict ptr。オブジェクトvoid**を変更しないと想定されているストアを介して同じですFoo*
Peter Cordes

4

このコードはC標準に従って無効であるため、場合によっては機能する可能性がありますが、必ずしも移植可能であるとは限りません。

別のポインター型にキャストされたポインターを介して値にアクセスするための「厳密なエイリアス規則」は、6.5段落7にあります。

オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つ必要があります。

  • オブジェクトの有効な型と互換性のある型

  • オブジェクトの有効な型と互換性のある型の修飾バージョン

  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型

  • オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型

  • メンバーの中に前述のタイプの1つを含む集約タイプまたはユニオンタイプ(再帰的に、サブアグリゲートのメンバーまたは含まれているユニオンを含む)、または

  • 文字タイプ。

あなたには*obj = NULL;声明、オブジェクトは、効果的なタイプがありますFoo*が、左辺値の式によってアクセスされる*objタイプvoid*

6.7.5.1パラグラフ2では、

2つのポインター型が互換性を持つためには、どちらも同じように修飾され、どちらも互換型へのポインターでなければなりません。

そのためvoid*Foo*互換性のある型や修飾子が追加された互換性のある型ではなく、厳密なエイリアスルールの他のオプションには適合しません。

コードが無効であるという技術的な理由ではありませんが、セクション6.2.5のパラグラフ26の注記にも関連しています。

へのポインタvoidは、文字型へのポインタと同じ表現と配置要件を持たなければならない。同様に、互換性のある型の修飾されたバージョンまたは修飾されていないバージョンへのポインターは、同じ表現と配置の要件を持たなければなりません。構造体型へのすべてのポインタは、互いに同じ表現と配置の要件を持たなければなりません。共用体型へのすべてのポインタは、互いに同じ表現と配置の要件を持たなければなりません。他の型へのポインターは、同じ表現または配置要件を持つ必要はありません。

警告の違いについては、これは標準が診断メッセージを必要とする場合ではないので、コンパイラーまたはそのバージョンが潜在的な問題に気づき、それらを役立つ方法で指摘するのにどれだけ優れているかだけの問題です。最適化の設定が違いを生むことに気づきました。これは、実際にはプログラムのさまざまな部分が実際にどのように組み合わされるかについてより多くの情報が内部で生成されるため、その追加情報は警告チェックにも利用できるためです。


2

他の回答が述べたことに加えて、これはCの古典的なアンチパターンであり、火で燃やされるべきものです。それはに現れます:

  1. あなたが警告を見つけたようなフリーアンドヌルアウト機能。
  2. 戻りの標準Cイディオムvoid *(これは、punning型の代わりに値の変換が含まれるため、この問題の影響を受けません)を回避し、代わりにエラーフラグを返し、ポインターを介して結果を格納する割り当て関数。

(1)の別の例として、ffmpeg / libavcodecのav_free機能に長年の悪名高い事例がありました。最終的にはマクロやその他のトリックで修正されたと思いますが、よくわかりません。

(2)の場合、cudaMallocposix_memalignは例です。

どちらの場合も、インターフェイスは本質的に無効な使用法を必要としませんが、それを強く推奨し、void *free-null-out機能の目的に反するタイプの追加の一時オブジェクトでのみ正しい使用法を認め、割り当てを厄介にします。


(1)がアンチパターンである理由について詳しく説明するリンクはありますか?私はこの状況/議論に慣れていないと思うので、もっと学びたいと思います。
StoneThrow、

1
@StoneThrow:それは本当に簡単です-意図は、解放されているメモリへのポインタを格納しているオブジェクトをnullにすることで誤用を防ぐことですが、実際にそれを行うことができる唯一の方法は、呼び出し元が実際にオブジェクトのポインタを格納している場合ですvoid *それを逆参照したいときはいつもそれをタイプし、キャスト/変換します。これは非常にまれです。呼び出し元が他のタイプのポインターを格納している場合、UBを呼び出さずに関数を呼び出す唯一の方法は、ポインターをタイプの一時オブジェクトにコピーし、void *そのアドレスを解放している関数に渡して、それからそれを行うだけです...
R .. GitHub ICE HELPING ICEを停止

1
...呼び出し元がポインタを持っている実際のストレージではなく、一時オブジェクトをnullにします。もちろん、実際に起こるのは、関数のユーザーが(void **)キャストを実行して、未定義の動作を生成することです。
R .. GitHub ICE

2

Cはすべてのポインターに同じ表現を使用するマシン用に設計されましたが、規格の作成者は、異なるタイプのオブジェクトへのポインターに異なる表現を使用するマシンで言語を使用できるようにしたいと考えました。したがって、さまざまな種類のポインターに異なるポインター表現を使用するマシンは、「あらゆる種類のポインターへのポインター」タイプをサポートする必要はありませんでした。

標準が作成される前は、すべてのポインター型に同じ表現を使用するプラットフォームの実装void**では、「適切なキャストを行うことで」を「ポインターへのポインター」として満場一致で使用できました。標準の作成者は、これがそれをサポートするプラットフォームで役立つことをほぼ確実に認識しましたが、それは普遍的にサポートすることができなかったので、それを義務付けることを拒否しました。その代わりに、彼らは、理にかなっているのが理にかなっている場合に、理論的根拠が「人気のある拡張」として説明するような構造を質の高い実装が処理することを期待していました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.