📌 この記事は 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 が 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 付きのコピーコンストラクタであればローカル格納できます。

試しに以下のように Anoexcept をつけてみてください:

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 のサイズ
GCC4 bytes32 bytes
Clang/libc++16 bytes48 bytes
MSVC16 bytes64 bytes
GCC は最も省メモリな設計で、関数オブジェクトを memcpy できるように限定することで、コピーやムーブの方法を記録するコストも回避しています。

参考リンク

まとめ

  • libstdc++(GCC) : is_trivially_copyable がローカル格納の条件。コピー可能だがトリビアルでないとヒープ確保。

  • libc++(Clang)・MSVC : noexcept 付きコピーコンストラクタであればヒープ確保せずローカル格納。

  • パフォーマンスと ABI のトレードオフ :GCC は高速・軽量だが制約が厳しい。libc++ は柔軟だがメモリコスト増。

このような細かい挙動の違いは、高パフォーマンスなコードやバグ回避 において重要なポイントになります。ぜひ、自分の環境で noexcept の有無やサイズを試してみてください。

ソースコードを読むと、すべてが明らかになります😉