C++標準ライブラリstd::function
のムーブ操作(ムーブコンストラクタ/ムーブ代入演算子)は保持する呼出し可能なオブジェクト(callable object)を必ずしもムーブせず、条件によってはコピーが行われる可能性がある。( ゚Д゚)ハァ?
C++標準ライブラリ仕様ではstd::function
ムーブ操作の結果として、下記の2種類の選択肢を与える:
- 呼出し可能オブジェクトの所有権がムーブ先に移動し、ムーブ元の
std::function
オブジェクトは呼出し不可能となる。 - 呼出し可能オブジェクトはコピーされ、ムーブ先/ムーブ元いずれの
std::function
オブジェクトも呼出し可能となる。
下記コードではラムダ式によるクロージャオブジェクトのコピー/ムーブ判別に、スマートポインタstd::shared_ptr
の参照カウント(use_count()
)を利用している。*1
// 動作追跡用スマートポインタ(参照カウントにのみ着目) auto sp = std::make_shared<int>(42); // fはスマートポインタを保持するクロージャオブジェクトを保持する std::function<long()> f = [sp=std::move(sp)]{ return sp.use_count(); }; std::cout << f() << " "; // 1を出力 // fからgへのムーブ操作 std::function<long()> g = std::move(f); // クロージャオブジェクトがムーブされた場合は参照カウント=1に // クロージャオブジェクトがコピーされた場合は参照カウント=2となる std::cout << g() << " "; // ★1 or 2を出力 // ★ムーブ後のfを呼び出す try { std::cout << f() << " "; // コピーされた場合は2を出力 // 注: ソースコード上は"ムーブ後"のためこの動作を期待すべきではない } catch (const std::bad_function_call&) { // ムーブ操作によりfが無効となりbad_function_call例外が送出される std::cout << ". "; } // ムーブ後のfを明示的にクリアする f = nullptr; std::cout << g(); // 1を出力
GCC 10.1による実行結果:
1 1 . 1
Clang 10.0による実行結果:
1 2 2 1
C++標準規格ではサイズが小さい呼出し可能オブジェクトを格納する際は動的メモリ確保を避けるよう推奨(encourage)しており、標準ライブラリ実装では Small Buffer Optimization(SBO) 技法(→id:yohhoy:20120428)が用いられる。GCCとClangではSBO利用の閾値が異なっており、前述の実行結果になったと考えられる。
auto sp = std::make_shared<int>(42); struct { char x[1000]; } large; // データメンバにlargeを含む大きなサイズのクロージャオブジェクトを保持するため // SBOは無効となり動的確保メモリ上にオブジェクト構築されると期待できる std::function<long()> f = [sp=std::move(sp), large] { (void)large; // (largeを使った処理) return sp.use_count(); }; // fからgへのムーブ操作 std::function<long()> g = std::move(f); std::cout << g(); // 1を出力(と期待できる)
std::function
クラステンプレートはムーブ元オブジェクトが “有効だが未規定な状態(valid but unspecified state)”(→id:yohhoy:20120616) としか要求せず、また演算子オーバーロードoperator()
仕様をふまえるとムーブコンストラクタ/代入演算子がコピー/ムーブ処理のいずれを行っても標準準拠といえる。もうちょっと何とかならんかったのか。*2
std::move
の代わりに [C++]std::exchangeによるmoveしてリセットするイディオムの御紹介 - 地面を見下ろす少年の足蹴にされる私 で紹介しているイディオムを用いると、C++プログラマの意図通りの動作を保証できる。
// fからgへの確実なムーブ処理 std::function<long()> g = std::exchange(f, {});
C++17 20.3.25, 23.14.13.2.1/p5-6, p16, p18-19より引用(下線部は強調)。
valid but unspecified state
a value of an object that is not specified except that the object's invariants are met and operations on the object behave as specified for its type
[Example: If an objectx
of typestd::vector<int>
is in a valid but unspecified state,x.empty()
can be called unconditionally, andx.front()
can be called only ifx.empty()
returnsfalse
. -- end example]
function(function&& f);
5 Postconditions: If!f
,*this
has no target; otherwise, the target of*this
is equivalent to the target off
before the construction, andf
is in a valid state with an unspecified value.
6 Throws: shall not throw exceptions iff
's target is a specialization ofreference_wrapper
or a function pointer. Otherwise, may throwbad_alloc
or any exception thrown by the copy or move constructor of the stored callable object. [Note: Implementations are encouraged to avoid the use of dynamically allocated memory for small callable objects, for example, wheref
's target is an object holding only a pointer or reference to an object and a member function pointer. -- end note]
function& operator=(function&& f);
16 Effects: Replaces the target of*this
with the target off
.
function& operator=(nullptr_t) noexcept;
18 Effects: If*this != nullptr
, destroys the target ofthis
.
19 Postconditions:!(*this)
.
関連URL
- c++ - Move semantic with std::function - Stack Overflow
- The space of design choices for `std::function` – Arthur O'Dwyer – Stuff mostly about C++
- ムーブの跡 - yohhoyの日記
- https://reviews.llvm.org/D55045
- cppreference: function<R(Args...)>::function, cpprefjp: function::コンストラクタ
- https://twitter.com/yohhoy/status/1325748115400351746