Y121516 の大館お役立ち日記

秋田県大館市での生活

forwardをmoveにしてしまうと悲惨

std::forwardstd::move にしてしまうと悲惨

C++ のお話。 std::forwardstd::move どっちを使えばいいか迷ったことはありますか?

超大雑把に言うと、テンプレートの文脈 (T&&) では std::forward を使って、具体的なオブジェクトを扱う場合は std::move を使います。 (ほんとうに大雑把なので注意。)

さて、それらを間違って使ってしまうとどうなるでしょうか?

コンパイル時に型チェックが入る C++ なら必ずエラーが出てくれるはず!!」…と思いきや、すんなりコンパイルが通って実行時に意味不明な挙動を示す最悪なパターンがあります。

その昔 libstdc++ で現実にあった

以下のコードをご覧ください。どのような実行結果を期待するでしょうか?

#include <memory>
#include <unordered_map>
#include <iostream>

int main(void)
{
  std::unordered_map<int, std::shared_ptr<int>> map;
  auto a(std::make_pair(5, std::make_shared<int>(5)));
  std::cout << "a.second is " << a.second.get() << std::endl;
  map.insert(a);
  std::cout << "a.second is now " << a.second.get() << std::endl;
  return 0;
}

gcc 4.8.1 での実行結果 [Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ:

a.second is 0xed8088
a.second is now 0

なんと 0 です。map.insert(a); しただけで a が壊れてしまっています。ひどくないですか?

原因はコンパイラではなく、libstdc++ にありました。問題の個所はこうなっていました。

template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

テンプレートの文脈 (_Pair&& __x) でstd::move(__x) ムーブしてしまっています。これが実行時に問題を引き起こしていました。恐ろしいですね。

このバグは gcc 4.8.2 の libstdc++ ですぐに修正されました。57619 – [4.8/4.9 Regression] std::unordered_map and std::unordered_multimap::insert invoking std::pair move constructor

対応作はあるの?

うっかりミスがコンパイル時に見つからないのは怖いですね。C++ なのに。 std::move はムーブコンストラクタ呼び出しのきっかけになり、ムーブコンストラクタが破壊的な挙動をします。最近の C++ は RVO や NRVO の適用範囲が広がっているので、std::move を極力書かないコーディングを心掛けたいですね。

正直、根本的にこのミスをしないようにする方法やコンパイラに必ずミスを見つけてもらう方法は思いつきません。もしいい方法があれば教えてください。