56

次の 2 つの関数を検討してください。

void foo() {}
void bar() {}

それは保証されてい&foo != &barますか?

同様に、

template<class T> void foo() { }

それは保証されてい&foo<int> != &foo<double>ますか?


私が知っている 2 つのリンカーは、関数定義を一緒に折り畳みます。

MSVC COMDAT は積極的に関数を折りたたむため、同じ実装を持つ 2 つの関数を 1 つの関数に変換できます。副作用として、2 つの関数は同じアドレスを共有します。これは違法であるという印象を受けましたが、標準のどこで違法になっているのかわかりません。

Gold リンカも関数を折り畳み、 asafeall設定の両方を使用します。 safeは、関数アドレスが取得された場合でも折り返されないことを意味しますが、 はallアドレスが取得されても折り畳まれます。したがって、gold's foldsafeは、あたかも関数が個別のアドレスを持っているかのように動作します。

折り畳みは予想外かもしれませんし、異なるアドレスを持つ別個の (同一の実装) 関数に依存するコードがあります (したがって折り畳むのは危険です) が、現在の C++ 標準では実際に違法ですか? (この時点では C++14) (当然 as-if のsafe折りたたみは合法です)

4

4 に答える 4

30

欠陥レポート 1400のように見えます: 関数ポインターの等価性がこの問題に対処しており、この最適化を行っても問題ないと私には思われますが、コメントが示すように、意見の相違があります。それは言います(私の強調):

5.10 [expr.eq] パラグラフ 2 によると、2 つの関数ポインタは、同じ関数を指している場合にのみ等しいと比較されます。ただし、最適化として、実装は現在、同一の定義を持つ関数をエイリアシングしています。標準がこの最適化を明示的に処理する必要があるかどうかは明らかではありません。

応答は次のとおりです。

標準は要件を明確にしており、実装は「as-if」ルールの制約内で自由に最適化できます

質問は2つの問題について尋ねています:

  • これらのポインターが等しいと見なされても問題ありませんか
  • 機能を合体させてもいいですか

コメントに基づいて、応答の 2 つの解釈が表示されます。

  1. この最適化は問題ありません。標準は、as-if ルールの下で実装にこの自由を与えます。as-if ルールはセクションで説明1.9されており、実装は標準の要件に関して観察可能な動作をエミュレートするだけでよいことを意味します。これはまだ応答の私の解釈です。

  2. 目前の問題は完全に無視されており、ステートメントは、仮定のルールがこれをカバーしていることは明らかであるため、標準への調整は不要であると述べているだけですが、解釈は読者の演習として残されています。回答が簡潔であるため、この見解を却下できないことは認めますが、最終的にはまったく役に立たない回答になります。またNAD、私が知る限り、問題が存在するかどうかを指摘している他の問題の回答とは矛盾しているようです。

ドラフト規格の内容

as-if ルールを扱っていることがわかっているので、そこから始めて、次のセクションに注意してください1.8

オブジェクトがビットフィールドまたはサイズがゼロの基本クラスのサブオブジェクトでない限り、そのオブジェクトのアドレスは、それが占有する最初のバイトのアドレスです。ビット フィールドではない 2 つのオブジェクトは、一方が他方のサブオブジェクトである場合、または少なくとも 1 つがサイズ 0 の基本クラス サブオブジェクトであり、それらが異なる型である場合、同じアドレスを持つことができます。それ以外の場合、それらは別個のアドレスを持つものとします。4

そしてメモ4は言う:

「as-if」ルールの下では、プログラムが違いを観察できない場合、実装は同じマシン アドレスに 2 つのオブジェクトを格納するか、オブジェクトをまったく格納しないことが許可されます。

しかし、そのセクションのメモには次のように書かれています。

オブジェクトのようにストレージを占有するかどうかに関係なく、関数はオブジェクトではありません

規範的ではありませんが、パラグラフで説明されているオブジェクトの要件は1、関数のコンテキストでは意味をなさないため、この注記と一致しています。そのため、いくつかの例外を除いて、オブジェクトのエイリアシングは明示的に制限されていますが、そのような制限は関数には適用されません。

次に、5.10 等値演算子のセクションがあります (強調鉱山):

[...] 2 つのポインターは、両方が null である場合、両方が同じ関数を指している場合、または両方が同じアドレス(3.9.2) を表している場合は等しいと見なされます。それ以外の場合は等しくありません。

これは、次の場合に 2 つのポインターが等しいことを示しています。

  • ヌル ポインター
  • 同じ関数を指す
  • 同じ住所を表す

または両方が同じアドレスを表すことで、コンパイラが 2 つの異なる関数にエイリアスを設定できるように十分な許容範囲が与えられ、異なる関数を比較するために異なる関数へのポインターを必要としないようです。

観察

Keith Thompson は、重要な問題に関係しているため、回答に追加する価値があると思われるいくつかの素晴らしい観察を行っています。

プログラムが&foo == &barの結果を出力する場合、それは観察可能な動作です。問題の最適化は、観察可能な動作を変更します。

私は同意します。ポインターが等しくないという要件があることを示すことができれば、実際にas-if ルールに違反しますが、これまでのところそれを示すことはできません。

と:

[...]空の関数を定義し、それらのアドレスを一意の値として使用するプログラムを考えてみましょう ( < signal.h> / <csignal>のSIG_DFLSIG_ERR、およびSIG_IGNについて考えてください)。それらに同じアドレスを割り当てると、そのようなプログラムが壊れます

私のコメントで指摘したように、C 標準では、これらのマクロがC11とは異なる値を生成する必要があります。7.14

[...]これは、シグナル関数の 2 番目の引数およびその戻り値と互換性のある型を持ち、その値が宣言可能な関数のアドレスと等しくない個別の値を持つ定数式に展開されます[...]

したがって、このケースはカバーされていますが、この最適化を危険にする他のケースがあるかもしれません。

アップデート

開発者である Jan Hubička は、GCC 5 でのリンク時間と手続き間の最適化の改善に関するgccブログ記事を書きました。コードの折り畳みは、彼がカバーした多くのトピックの 1 つです。

私は彼に、同一の関数を同じアドレスに折りたたむことが適合動作であるかどうかについてコメントするように依頼しましたgcc.

ターン 2 関数が同じアドレスを持つように準拠していないため、MSVC はここで非常に積極的です。たとえば、これを行うと、GCC 自体が壊れます。これは、驚いたことに、プリコンパイル済みヘッダー コードでアドレス比較が行われるためです。Firefox を含む他の多くのプロジェクトで機能します。

振り返ってみると、何ヶ月も欠陥レポートを読み、最適化の問題について考えた結果、私は委員会の反応をより保守的に読む傾向にありました。関数のアドレスを取得することは観察可能な動作であるため、同一の関数を折りたたむことはas-if ルールに違反します。

更新 2

このllvm-dev の議論も参照してください: 長さゼロの関数ポインタの等価性:

これは、link.exe のよく知られた適合性違反のバグです。LLVM 自体が同様のバグを導入して事態を悪化させるべきではありません。よりスマートなリンカー (たとえば、lld と gold の両方だと思います) は、関数シンボルの 1 つを除くすべてが呼び出しのターゲットとしてのみ使用される場合にのみ (そして実際にアドレスを観察しない場合)、同一の関数を組み合わせて実行します。そして、はい、この不適合な動作は (めったに) 実際には物事を壊します。この 研究論文を参照してください。

于 2014-10-23T18:44:39.863 に答える
11

はい。標準 (§5.10/1) から: 「同じ型の 2 つのポインターは、両方が null であるか、両方が同じ関数を指しているか、または両方が同じアドレスを表している場合にのみ、等しいと比較されます」

それらがインスタンス化されるfoo<int>foo<double>、2 つの異なる機能になるため、上記はそれらにも適用されます。

于 2014-10-23T17:34:22.143 に答える
9

したがって、問題のある部分は明らかにフレーズまたは両方が同じアドレス (3.9.2) を表していることです。

IMO この部分は、オブジェクト ポインター型のセマンティクスを定義するために明らかに存在します。また、オブジェクト ポインター型のみ。

このフレーズはセクション 3.9.2 を参照しており、そこを参照する必要があることを意味します。3.9.2 では、オブジェクト ポインターが表すアドレスについて (とりわけ) 説明しています。関数ポインタが表すアドレスについては触れていません。IMO では、考えられる解釈は 2 つだけです。

1) このフレーズは単に関数ポインタには当てはまりません。これにより、同じ関数を比較する 2 つの null ポインターと 2 つのポインターだけが残ります。これは、おそらくほとんどの人が予想していたことです。

2) このフレーズは当てはまります。関数ポインターが表すアドレスについて何も述べていない 3.9.2 を参照しているため、任意の 2 つの関数ポインターを比較して等しくすることができます。これは非常に予想外であり、もちろん、関数ポインターの比較はまったく役に立たないものになります。

したがって、技術的には (2) が有効な解釈であるという議論を行うことができますが、IMO は意味のある解釈ではないため、無視する必要があります。そして、誰もがこれに同意しているわけではないので、標準の明確化も必要だと思います.

于 2014-10-24T18:14:43.807 に答える
3

5.10 等値演算子[expr.eq]

1 ==(等しい) および (等しく!=ない) 演算子は、左から右にグループ化されます。オペランドには、算術、列挙、ポインター、またはメンバー型へのポインター、または type が必要std::nullptr_tです。演算子==!=両方がtrueorfalseを生成します。つまり、 type の結果ですbool以下の各ケースでは、指定された変換が適用された後、オペランドは同じ型を持つものとします。
2オペランドの少なくとも 1 つがポインタである場合、両方のオペランドに対してポインタ変換 (4.10) と修飾変換 (4.4) が実行され、複合ポインタ型になります(条項 5)。ポインターの比較は、次のように定義されます。2 つのポインターは、両方が null である場合、または両方が同じ関数を指している場合、または両方が同じアドレス (3.9.2) を表している場合は等しく、そうでない場合は等しくありません。

最後のビットごとに見てみましょう。

  1. 2 つのヌル ポインターは等しいと比較されます。
    あなたの正気のために良い。
  2. 同じ関数への 2 つのポインターは等しいと比較されます。
    それ以外は非常に驚くべきことです。また、関数ポインタの比較を非常に複雑で高価にしたくない場合を除き、任意の -function
    の 1 つのアウトオブライン バージョンだけがそのアドレスを取得できることも意味します。inline
  3. どちらも同じアドレスを表します。
    今、それがすべてです。if and only ifこれを削除して単純なものに減らすifと解釈に委ねられますが、準拠プログラムの観察可能な動作を変更しない限り、2 つの関数を同一にすることが明確に義務付けられています。
于 2014-10-23T18:18:25.430 に答える