📌 この記事は Zenn に投稿した内容のアーカイブです。
はじめに
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が falseBはコピーコンストラクタが定義されておらず、コンパイラが暗黙定義 → トリビアルコピー可能
そのため、[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 の有無やサイズを試してみてください。
ソースコードを読むと、すべてが明らかになります😉