はじめに
std::accumulate/reduce()には、「第三引数に生の0
を渡すと、int
として誤集計してしまう」という罠があります。
競プロでもプロダクトでも、これが発生するととても辛い気持ちになりそうです。
それにもかかわらず、これを防ぐ方法が日本語で見当たらなかったので、この記事を書きました。
現状広く知れ渡ったお作法ではありませんが、accumulate()を書いている全人類にオススメしたいです。
(2021.9.11 追記) C++17が書ける環境では黙ってreduce()を使いましょう。@yumetodoさんのコメントに感謝。
float v[] = {1.1, 1.2, 1.4};
// std::vector<float> v = {1.1f, 1.2f, 1.4f};
// std::array v = {1.1f, 1.2f, 1.4f};
float sum = std::reduce(std::begin(v), std::end(v)); // どの型でも無事3.7
std::cout << sum << std::endl;
以下はC++17が使えず、C++11が使える人向けの記事です。
お作法
accumulateの第三引数には、std::remove_refrerence~
以下の部分を渡します。
vector<int> v = {1, 2, 4};
int sum = accumulate(v.begin(), v.end(), std::remove_reference<decltype(*std::begin(v))>::type{}); // => 7
以上です。
std::vector以外にも普通の配列, std::*set, std::arrayでも使えます。
accumulate の罠
お作法に従わないと以下の罠にハマる可能性があります。
vector<float> v = {1.1, 1.2, 1.4};
float sum = accumulate(v.begin(), v.end(), 0); // 3.7になるはずが、3が出力される!
理由は、初期値に生の0
を渡したせいで、リテラル的にintと解釈され、floatがintとして誤集計されてしまうからです。
この意図しないダウンキャストを防ぐために、decltype(v)::value_type(0)
を渡すというのが今回のお作法です。
vector<float> v = {1.1, 1.2, 1.4};
float sum = accumulate(v.begin(), v.end(), std::remove_reference<decltype(*std::begin(v))>::type{}); // 無事3.7
これは「v
というコンテナの要素型を0
で初期化した値」で、実質(float) 0
を渡したことになります。よって、floatとして集計され、無事に3.7
が出力されました。
float
以外の事例では、int64_t
やlong long
をintとして誤集計してしまいオーバーフローしたといったことがあるようです。
std::arrayじゃない方の静的配列の場合
stdじゃない方のarrayはdecltype()が使えないので、残念ながらお作法が適用できません。
(2021.9.11 追記)
decltype(v)::value_type(0)
→ remove_reference<decltype(std::begin(v))>::type{}
とすることで対応できました。
おわりに
C++強い勢はC++が強い
参考
- お作法の参考 https://stackoverflow.com/a/3221813 のImportant Note
- int64_tでの事例 https://airscarlet.com/atcoder-accumulate_64bit/
- accumulate https://qiita.com/haruyama480/items/a7ddfc50a03e1f6258dd