C++におけるabs関数のオーバーロードについて調べた
目的
https://ja.cppreference.com/w/cpp/header によると,cmath
やcstdlib
などのC互換ヘッダはstd
名前空間で宣言した関数をグローバル名前空間でも宣言するかもしれない.
実際,以下のソースコードをコンパイルしてみると,グローバル名前空間のabs
関数とstd
名前空間のabs
関数の両方を参照できることが分かる.
#include <cmath> #include <cstdlib> #include <iostream> using ll = long long int; int main() { ll x = 1e9; std::cout << ::abs(x * x) << std::endl; std::cout << std::abs(x * x) << std::endl; }
- コンパイルコマンド
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment -o main main.cpp
しかし,以下のように生成された実行ファイルを実行してみると,グローバル名前空間のabs
関数とstd
名前空間のabs
関数が異なる結果を出力していることが分かる.
- 実行コマンド
./main
- 実行結果
1486618624 1000000000000000000
このプログラムは(109) * (109)の絶対値を求めようするものであるが,グローバル名前空間のabs
関数を使うと正しい値を求められない.
本記事では,グローバル名前空間のabs
関数とstd
名前空間のabs
関数がどのように解決されるかを確かめ,この問題の対策を考える.
検証
https://cpprefjp.github.io/reference/cstdlib.html によると,cstdlib
はint
型の絶対値を求めるint abs(int)
を宣言している.
また,https://cpprefjp.github.io/reference/cmath/abs.html によると,cmath
はint abs(int)
に加えて,long long int
型の絶対値を求めるlong long int abs(long long int)
を宣言している.
そこで,グローバル名前空間のabs
関数とstd
名前空間のabs
関数に対して,上のプログラムのようにlong long int
型の引数を渡した時,これらの関数がそれぞれint abs(int)
とlong long int abs(long long int)
のどちらに解決されるのかを確かめるために,以下のソースコードをコンパイルしてみた.
ソースコード
#include <cmath> #include <cstdio> #include <cstdlib> #include <type_traits> using std::is_same_v, std::declval; using ll = long long int; int main() { static_assert(is_same_v<decltype(::abs(declval<ll>())), int>); static_assert(is_same_v<decltype(std::abs(declval<ll>())), ll>); }
コンパイル
検証は以下の環境で行った.
以下のコマンドによりコンパイルを行い,実行ファイルmain
を得た.
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment -o main main.cpp
結果
上のソースコードがコンパイルに成功したことから,以下のことが分かる.
- グローバル名前空間の
abs
関数にlong long int
型の引数を渡すとint abs(int)
に解決される. std
名前空間のabs
関数にlong long int
型の引数を渡すとlong long int abs(long long int)
に解決される.
考察
検証結果から最初に示したプログラムで,グローバル名前空間のabs
関数を使った場合に正しい値を求められなかった原因は次のようなものだと考えられる.
グローバル名前空間のabs
にlong long int
型の引数を渡すと,abs
がint abs(int)
に解決されるため,引数がint
型に変換される.
この時,(109) * (109) == 1018はint型で表現するには大きすぎるためオーバーフローが発生し,正しくない値がabs
に渡ってしまう.
対策
グローバル名前空間のint abs(int)
関数にlong long int
型の引数を渡してしまう誤りはコンパイルエラーとして検出できない.
また,long long int
型の値の絶対値を求めるときはllabs
を使うように意識するという方法や,abs
関数の前に常に「std::」を付けるように意識するという方法は,ミスの無い完璧な人間は存在しないということを考えれば,対策として十分とは言えない.
したがって,一般的な対策はとても難しいと考えられる.
しかし,対象をプログラミングコンテストなどに限れば以下のような対策が実施可能である.
予めテンプレートとなるソースコードを用意しておくことが可能なAtCoder(https://atcoder.jp)のようなプログラミングコンテストでは,そのテンプレートに次の記述を追加しておくことで,グローバル名前空間のint abs(int)
関数にlong long int
型の引数を渡してしまう誤りをなくすことができる.
#include<cmath> using std::abs;
このusing
宣言により,グローバル名前空間にstd
名前空間のabs
関数が導入されるため,long long int
型をグローバル名前空間のabs
関数の引数にしようとした場合でもlong long int abs(long long int)
が参照されるようになる.
実際,最初のソースコードにこれを追加して実行すると正しい結果が得られる.
#include <cmath> #include <cstdlib> #include <iostream> using ll = long long int; using std::abs; int main() { ll x = 1e9; std::cout << ::abs(x * x) << std::endl; std::cout << std::abs(x * x) << std::endl; }
- 実行結果
1000000000000000000 1000000000000000000