std::function がヒープメモリを確保する条件とは?— コピーキャプチャされた変数の型がカギ
はじめに
C++ の std::function
は「小さいオブジェクトならヒープ確保しない(Small Object Optimization, SOO)」という知識は一般的です。しかし、以下のようなコードでは意外な挙動が観察されます:
struct A {
int value;
A(int v) : value(v) {}
A(const A &other) : value(other.value) {}
};
struct B {
int value;
B(int v) : value(v) {}
};
A a{0};
B b{0};
std::function<void()> funcA = [a] {}; // ヒープ確保あり
std::function<void()> funcB = [b] {}; // ヒープ確保なし
このように、コピーコンストラクタの有無だけで挙動が変わるのはなぜでしょうか?
std::function
がローカル格納を行う条件(GCC/libstdc++ 編)
libstdc++ の実装(GCC 標準ライブラリ)では、以下の条件を満たすとオブジェクトはヒープではなくローカルに保存されます:
static const bool __stored_locally =
(__is_location_invariant<_Functor>::value &&
sizeof(_Functor) <= _M_max_size &&
__alignof__(_Functor) <= _M_max_align &&
(_M_max_align % __alignof__(_Functor) == 0));
この中で特に重要なのが:
template<typename _Tp>
struct __is_location_invariant : is_trivially_copyable<_Tp>::type {};
つまり、トリビアルコピー可能(is_trivially_copyable
)であることがローカル格納の前提条件です。
A
はユーザー定義のコピーコンストラクタを持っているため、is_trivially_copyable
が false
B
はコピーコンストラクタが定義されておらず、コンパイラが暗黙定義 → トリビアルコピー可能
そのため、[a]{}
はヒープ確保、[b]{}
はローカル格納となります。
libc++(Clang)や MSVC の挙動は?
Clang の libc++ や MSVC の std::function
実装では、GCC より条件が緩和されています。たとえば libc++ では以下の条件です:
if (sizeof(_Fun) <= sizeof(__buf_) &&
is_nothrow_copy_constructible<_Fp>::value &&
is_nothrow_copy_constructible<_FunAlloc>::value)
{
// ローカル格納
}
つまり、トリビアルである必要はなく、noexcept
付きのコピーコンストラクタであればローカル格納できます。
試しに以下のように A
に noexcept
をつけてみてください:
A(const A &other) noexcept : value(other.value) {}
この変更だけで、std::function
のヒープ確保がなくなります。
✅ Godbolt 上で確認する
std::function
サイズとヒープ確保の違い
各コンパイラでの 以下のコードで std::function
自体のサイズを比較してみます:
std::function<void()> func = [a]{};
printf("%zu\n", sizeof(func));
結果(x64 プラットフォーム):
コンパイラ | ヒープ確保サイズ |
std::function のサイズ |
---|---|---|
GCC | 4 bytes | 32 bytes |
Clang/libc++ | 16 bytes | 48 bytes |
MSVC | 16 bytes | 64 bytes |
GCC は最も省メモリな設計で、関数オブジェクトを memcpy できるように限定することで、コピーやムーブの方法を記録するコストも回避しています。 |
参考リンク
まとめ
-
libstdc++(GCC):
is_trivially_copyable
がローカル格納の条件。コピー可能だがトリビアルでないとヒープ確保。 -
libc++(Clang)・MSVC:
noexcept
付きコピーコンストラクタであればヒープ確保せずローカル格納。 -
パフォーマンスと ABI のトレードオフ:GCC は高速・軽量だが制約が厳しい。libc++ は柔軟だがメモリコスト増。
このような細かい挙動の違いは、高パフォーマンスなコードやバグ回避において重要なポイントになります。ぜひ、自分の環境で noexcept
の有無やサイズを試してみてください。
ソースコードを読むと、すべてが明らかになります😉
Discussion
バージョンなどが確認可能な検証用のコードだけ貼っておきます。
こういうのがないと他の人が「うんそうだね」って言えないので。あと、ラムダ式というより、ラムダ式でコピーキャプチャする変数の話ですよね。
後ろで書いた間違い以外にも、私のリンクでもgccがないというか、godboltでうまく復元できてないようでした。なので、修正した後いくつかのブラウザで確認してOKとなったリンク先を追記しておきます。
ありがとうございます!
おっしゃる通りで、今回の話はラムダ式そのものというよりも、コピーキャプチャされる変数の型特性にフォーカスすべき内容ですね。
また、再現コードやコンパイラバージョンを明示するのもとても大切ですね。
ご指摘いただいて助かりました。検証用コードを追記しました!
一応確認してみたのですが、最初の
のリンク先コードにgcc(libstdc++)がありません。
また出来ればnoexceptが入っているコードと入っていないコードは分けて確認できた方がいいと思います。
あと私のコードにも間違いがありましたね。global newの定義でbad_alloc出すつもり満々でnothrow(false)にしておきながら、出してませんでした。nothrowに読み替えてください(>皆様)。