#include <iostream>
#include <cmath>
/* Intentionally incorrect abs() which seems to override std::abs() */
int abs(int a) {
return a > 0? -a : a;
}
int main() {
int a = abs(-5);
int b = std::abs(-5);
std::cout<< a << std::endl << b << std::endl;
return 0;
}
出力は-5
と5
になると予想していましたが、出力は-5
と-5
です。
この事件はなぜ起こるのだろうか?
std
の使用とは何か関係がありますか?
言語仕様 許可 実装は<cmath>
を実装し、global名前空間で標準関数を宣言(および定義)し、それらを名前空間std
に持ち込むusing宣言を使用します。このアプローチが使用されるかどうかは不明です
20.5.1.2ヘッダー
4[...]ただし、C++標準ライブラリでは、宣言(Cでマクロとして定義されている名前を除く) )は、名前空間std
の名前空間スコープ(6.3.6)内にあります。これらの名前(条項21から33およびAnnex Dで追加されたオーバーロードを含む)が最初にグローバル名前空間スコープ内で宣言され、次に明示的なusing宣言(10.3.3)によって名前空間std
に注入されるかどうかは指定されていません。
どうやら、このアプローチに従うことにした実装の1つ(GCCなど)を扱っているようです。つまり実装は::abs
を提供しますが、std::abs
は単に::abs
を「参照」します。
この場合に残る疑問の1つは、標準の::abs
に加えて、独自の::abs
を宣言できた理由、つまり多重定義エラーがない理由です。これは、いくつかの実装(GCCなど)によって提供される別の機能が原因である可能性があります。標準関数をいわゆる弱いシンボルとして宣言し、独自の定義で「置換」できるようにします。
これらの2つの要因が一緒になって、観察する効果を生み出します。::abs
の弱いシンボルの置換は、std::abs
の置換ももたらします。これが言語標準とどれだけよく一致するかは別の話です...いずれにしても、この動作に依存しないでください-言語によって保証されていません。
GCCでは、この動作を次の最小限の例で再現できます。 1つのソースファイル
#include <iostream>
void foo() __attribute__((weak));
void foo() { std::cout << "Hello!" << std::endl; }
別のソースファイル
#include <iostream>
void foo();
namespace N { using ::foo; }
void foo() { std::cout << "Goodbye!" << std::endl; }
int main()
{
foo();
N::foo();
}
この場合、2番目のソースファイルの::foo
("Goodbye!"
)の新しい定義がN::foo
の動作にも影響することもわかります。両方の呼び出しは"Goodbye!"
を出力します。また、2番目のソースファイルから::foo
の定義を削除すると、両方の呼び出しが::foo
の「元の」定義にディスパッチされ、"Hello!"
が出力されます。
上記の20.5.1.2/4によって与えられる許可は、<cmath>
の実装を簡素化するためにあります。実装では、単にCスタイル<math.h>
を含めてから、std
で関数を再宣言し、C++固有の追加および調整を追加できます。上記の説明が問題の内部メカニズムを適切に説明している場合、その大部分は、関数のCスタイルバージョンの弱いシンボルの置換可能性に依存しています。
上記のプログラムでint
を単純にdouble
でグローバルに置き換える場合、コード(GCCの下)は「期待どおり」に動作し、-5 5
を出力することに注意してください。これは、C標準ライブラリにabs(double)
関数がないために発生します。独自のabs(double)
を宣言することにより、何も置き換えません。
ただし、int
からdouble
に切り替えた後、abs
からfabs
に切り替えると、元の奇妙な動作が完全に再現されます(出力-5 -5
)。
これは上記の説明と一致しています。
コードにより未定義の動作が発生します。
C++ 17 [extern.names]/4:
外部リンケージで宣言されたC標準ライブラリの各関数シグネチャは、extern "C"とextern "C++"リンケージの両方の関数シグネチャとして、またはグローバル名前空間の名前空間スコープの名前として使用するために実装に予約されます。
したがって、標準Cライブラリ関数int abs(int);
と同じプロトタイプを持つ関数を作成することはできません。実際にどのヘッダーを含めるか、またはこれらのヘッダーがCライブラリ名をグローバル名前空間に入れるかどうかに関係なく。
ただし、異なるパラメータータイプを指定すると、abs
をオーバーロードすることができます。