C++におけるabs関数のオーバーロードについて調べた

目的

https://ja.cppreference.com/w/cpp/header によると,cmathcstdlibなどの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 によると,cstdlibint型の絶対値を求めるint abs(int)を宣言している. また,https://cpprefjp.github.io/reference/cmath/abs.html によると,cmathint 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関数を使った場合に正しい値を求められなかった原因は次のようなものだと考えられる. グローバル名前空間abslong long int型の引数を渡すと,absint abs(int)に解決されるため,引数がint型に変換される. この時,(109) * (109) == 1018はint型で表現するには大きすぎるためオーバーフローが発生し,正しくない値がabsに渡ってしまう.

対策

グローバル名前空間int abs(int)関数にlong long int型の引数を渡してしまう誤りはコンパイルエラーとして検出できない. また,long long int型の値の絶対値を求めるときはllabsを使うように意識するという方法や,abs関数の前に常に「std::」を付けるように意識するという方法は,ミスの無い完璧な人間は存在しないということを考えれば,対策として十分とは言えない. したがって,一般的な対策はとても難しいと考えられる.

しかし,対象をプログラミングコンテストなどに限れば以下のような対策が実施可能である. 予めテンプレートとなるソースコードを用意しておくことが可能なAtCoderhttps://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