🎉

std::function がヒープメモリを確保する条件とは?— コピーキャプチャされた変数の型がカギ

に公開3

はじめに

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 のサイズ
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

dameyodamedamedameyodamedame

バージョンなどが確認可能な検証用のコードだけ貼っておきます。
https://godbolt.org/z/qM8rd9eGf
こういうのがないと他の人が「うんそうだね」って言えないので。

あと、ラムダ式というより、ラムダ式でコピーキャプチャする変数の話ですよね。


後ろで書いた間違い以外にも、私のリンクでもgccがないというか、godboltでうまく復元できてないようでした。なので、修正した後いくつかのブラウザで確認してOKとなったリンク先を追記しておきます。

https://godbolt.org/z/K79b9oGxE

wuyukwiwuyukwi

ありがとうございます!
おっしゃる通りで、今回の話はラムダ式そのものというよりも、コピーキャプチャされる変数の型特性にフォーカスすべき内容ですね。
また、再現コードやコンパイラバージョンを明示するのもとても大切ですね。
ご指摘いただいて助かりました。検証用コードを追記しました!

dameyodamedamedameyodamedame

一応確認してみたのですが、最初の

Godbolt 上で確認する

のリンク先コードにgcc(libstdc++)がありません。
また出来ればnoexceptが入っているコードと入っていないコードは分けて確認できた方がいいと思います。


あと私のコードにも間違いがありましたね。global newの定義でbad_alloc出すつもり満々でnothrow(false)にしておきながら、出してませんでした。nothrowに読み替えてください(>皆様)。