web-dev-qa-db-ja.com

C ++ std :: unique_ptr:ラムダにサイズ料金がないのはなぜですか?

「EffectiveModernC++」を読んでいます。 _std::unique_ptr_に関連する項目では、カスタムデリータがステートレスオブジェクトの場合はサイズ料金は発生しないが、関数ポインタの場合は_std::function_サイズ料金が発生すると記載されています。理由を説明していただけますか?

次のコードがあるとしましょう。

_auto deleter_ = [](int *p) { doSth(p); delete p; };
std::unique_ptr<int, decltype(deleter_)> up(new int, deleter_);
_

私の理解では、_unique_ptr_はタイプdecltype(deleter_)のオブジェクトを持ち、その内部オブジェクトに_deleter__を割り当てる必要があります。しかし、明らかにそれは起こっていることではありません。可能な限り最小のコード例を使用して、この背後にあるメカニズムを説明できますか?

unique_ptrは常にその削除機能を保存する必要があります。これで、削除者が状態のないクラスタイプである場合、unique_ptr空の基本最適化 を利用できるため、削除者は追加のスペースを使用しません。

これがどの程度正確に行われるかは、実装によって異なります。たとえば、 libc ++ とMSVCの両方が、マネージポインターとデリッターを 圧縮ペア に格納します。これにより、関連するタイプの1つが空のクラスである場合、空の基本最適化が自動的に行われます。 。

上記のlibc ++リンクから

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TYPE_VIS_ONLY unique_ptr
{
public:
    typedef _Tp element_type;
    typedef _Dp deleter_type;
    typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
private:
    __compressed_pair<pointer, deleter_type> __ptr_;

libstdc ++ 2つを格納しますstd::Tupleにあり、一部のGoogle検索では、Tupleの実装で空のベース最適化を採用していることが示唆されていますが、そのように明示的に記載されているドキュメントは見つかりません。

いずれにせよ、 この例 は、libc ++とlibstdc ++の両方がEBOを使用して、空のデリッターでunique_ptrのサイズを縮小することを示しています。

37
Praetorian

削除機能がステートレスの場合、削除機能を保存するためのスペースは必要ありません。削除機能がステートレスでない場合は、状態をunique_ptr自体に格納する必要があります。
std::functionおよび関数ポインターには、実行時にのみ使用可能な情報があるため、オブジェクト自体のポインターと一緒にオブジェクトに格納する必要があります。これには、その余分な状態を格納するために(unique_ptr自体に)スペースを割り当てる必要があります。

おそらく Empty Base Optimization を理解することは、これを実際にどのように実装できるかを理解するのに役立ちます。
std::is_empty タイプ特性は、これを実装する方法のもう1つの可能性です。

ライブラリの作成者がこれをどの程度正確に実装するかは、明らかに彼らと標準で許可されていること次第です。

19
SirGuy

unique_ptr実装から:

template<class _ElementT, class _DeleterT = std::default_delete<_ElementT>>
class unique_ptr
{
public:
   // public interface...

private:

  // using empty base class optimization to save space
  // making unique_ptr with default_delete the same size as pointer

  class _UniquePtrImpl : private deleter_type
  {
  public:
     constexpr _UniquePtrImpl() noexcept = default;

     // some other constructors...

     deleter_type& _Deleter() noexcept
     { return *this; }

     const deleter_type& _Deleter() const noexcept
     { return *this; }

     pointer& _Ptr() noexcept
     { return _MyPtr; }

     const pointer _Ptr() const noexcept
     { return _MyPtr; }

  private:
     pointer   _MyPtr;

  };

  _UniquePtrImpl   _MyImpl;

};

_UniquePtrImplクラスにはポインタが含まれ、deleter_typeから派生します。

デリータがステートレスである場合は、基本クラスを最適化して、それ自体にバイトがかからないようにすることができます。その場合、unique_ptr全体を、含まれているポインターと同じサイズにすることができます。つまり、通常のポインターと同じサイズにすることができます。

15
Bo Persson

実際、ステートレスではないラムダ、つまり1つ以上の値をキャプチャするラムダにはサイズペナルティがありますwill

ただし、非キャプチャラムダの場合、注意すべき2つの重要な事実があります。

  • ラムダのタイプはniqueであり、コンパイラーのみが認識します。
  • キャプチャしないラムダはステートレスです。

したがって、コンパイラは、unique_ptrのタイプの一部として記録されるtypeに基づいてラムダを呼び出すことができます。追加のランタイム情報は必要ありません。

これが、非キャプチャラムダがステートレスである理由です。サイズペナルティの質問に関しては、もちろん、他のステートレス削除ファンクタータイプと比較して、非キャプチャラムダについて特別なことは何もありません。

std::functionnotステートレスであることに注意してください。そのため、同じ理由がnotに適用されます。

最後に、ステートレスオブジェクトは通常、一意のアドレスを持つためにゼロ以外のサイズである必要がありますが、ステートレス基本クラスは、派生型の合計サイズに追加する必要がありますnot。これは空のベース最適化と呼ばれます。したがって、unique_ptrは(Bo Perrsonの回答のように)デリータ型から派生した型として実装できます。これは、ステートレスの場合、サイズのペナルティにはなりません。 (これは実際には、ステートレス削除機能のサイズペナルティなしでunique_ptrを正しく実装するためののみ方法かもしれませんが、よくわかりません。)

6
Kyle Strand