C++ folly庫解讀(二) small_vector —— 小資料集下的std::vector替代方案

張雅宸發表於2021-02-28

介紹

行為與std::vector類似,但是使用了small buffer optimization(類似於fbstring中的SSO),將指定個數的資料內聯在物件中,而不是像std::vector直接將物件分配在堆上,避免了malloc/free的開銷。

small_vector基本相容std::vector的介面。

small_vector<int,2> vec;

vec.push_back(0);     // Stored in-place on stack
vec.push_back(1);     // Still on the stack
vec.push_back(2);    // Switches to heap buffer.

small_vector<int,2> vec指定可以內聯在物件中2個資料:

image

當超過2個後,後續新增的資料會被分配到堆上,之前的2個資料也會被一起move到堆上:

image

使用場景

根據官方文件的介紹,small_vector在下面3種場景中很有用:

  • 需要使用的vector的生命週期很短(比如在函式中使用),並且存放的資料佔用空間較小,那麼此時節省一次malloc是值得的。
  • 如果vector大小固定且需要頻繁查詢,那麼在絕大多數情況下會減少一次cpu cache miss,因為資料是內聯在物件中的。
  • 如果需要建立上億個vector,而且不想在記錄capacity上浪費物件空間(一般的vector物件內會有三個欄位:pointer _Myfirst、pointer _Mylast、pointer _Myend)。small_vector允許讓malloc來追蹤allocation capacity(這會顯著的降低insertion/reallocation效率,如果對這兩個操作的效率比較在意,你應該使用FBVector,FBVector在官方描述中可以完全代替std::vector)

比如在io/async/AsyncSocket.h中,根據條件的不同使用small_vector或者std::vector:

  // Lifecycle observers.
  //
  // Use small_vector to avoid heap allocation for up to two observers, unless
  // mobile, in which case we fallback to std::vector to prioritize code size.
using LifecycleObserverVecImpl = conditional_t<
      !kIsMobile,
      folly::small_vector<AsyncTransport::LifecycleObserver*, 2>,
      std::vector<AsyncTransport::LifecycleObserver*>>;

LifecycleObserverVecImpl lifecycleObservers_;


// push_back
void AsyncSocket::addLifecycleObserver(
    AsyncTransport::LifecycleObserver* observer) {
  lifecycleObservers_.push_back(observer);
}

// for loop
for (const auto& cb : lifecycleObservers_) {
    cb->connect(this);
}

// iterator / erase
 const auto eraseIt = std::remove(
      lifecycleObservers_.begin(), lifecycleObservers_.end(), observer);
  if (eraseIt == lifecycleObservers_.end()) {
    return false;
}

為什麼不是std::array

下面兩種情況,small_vector比std::array更合適:

  • 需要空間後續動態增長,不僅僅是編譯期的固定size。
  • 像上面的例子,根據不同條件使用std::vector/small_vector,且使用的API介面是統一的。

其他用法

  • NoHeap : 當vector中資料個數超過指定個數時,不會再使用堆。如果個數超過指定個數,會丟擲std::length_error異常。
  • <Any integral type> : 指定small_vector中size和capacity的資料型別。
// With space for 32 in situ unique pointers, and only using a 4-byte size_type.
small_vector<std::unique_ptr<int>, 32, uint32_t> v;

// A inline vector of up to 256 ints which will not use the heap.
small_vector<int, 256, NoHeap> v;

// Same as the above, but making the size_type smaller too.
small_vector<int, 256, NoHeap, uint16_t> v;

其中,依賴boost::mpl超程式設計庫,可以讓後兩個模板變數任意排序。

其他類似庫

Benchmark

沒有找到官方的benchmark,自己簡單的寫了一個,不測試資料溢位到堆上的情況。

插入4個int,std::vector使用reserve(4)預留空間。

BENCHMARK(stdVector, n) {
  FOR_EACH_RANGE(i, 0, n) {
    std::vector<int> vec;
    vec.reserve(4);

    for (int i = 0; i < 4; i++) {
      vec.push_back(1);
    }

    doNotOptimizeAway(vec);
  }
}

BENCHMARK_DRAW_LINE();

BENCHMARK_RELATIVE(smallVector, n) {
  FOR_EACH_RANGE(i, 0, n) {
    small_vector<int, 4> vec;

    for (int i = 0; i < 4; i++) {
      vec.push_back(1);
    }

    doNotOptimizeAway(vec);
  }
}

結果是:small_vector比std::vector快了40%:

============================================================================
delve_folly/benchmark.cc                        relative  time/iter  iters/s
============================================================================
stdVector                                                  440.79ns    2.27M
----------------------------------------------------------------------------
smallVector                                      140.48%   313.77ns    3.19M
============================================================================

如果把stdVector中的vec.reserve(4);去掉,那麼small_vector速度比std::vector快了3倍。在我的環境上,std::vector的擴容因子為2,如果不加reserve,那麼std::vector會有2次擴容的過程(貌似很多人不習慣加reserve,是有什麼特別的原因嗎 : )):

============================================================================
delve_folly/benchmark.cc                        relative  time/iter  iters/s
============================================================================
stdVector                                                    1.26us  795.06K
----------------------------------------------------------------------------
smallVector                                      417.25%   301.44ns    3.32M
============================================================================

程式碼關注點

small_vector程式碼比較少,大概1300多行。主要關注以下幾個方面:

  • 主要類。
  • 資料儲存結構,capacity的儲存會複雜一些。
  • 擴容過程,包括資料從物件中遷移到堆上。
  • 常用的函式,例如push_back、reserve、resize。
  • 使用makeGuard代替了原版的try-catch。
  • 通過boost::mpl支援模板引數任意排序。
  • 通過boost-operators簡化operator以及C++20的<=>。

主要類

image

  • small_vector : 包含的核心欄位為union Datastruct HeapPtrstruct HeapPtrWithCapacity,這三個欄位負責資料的儲存。此外small_vector對外暴露API介面,例如push_back、reserve、resize等。
  • small_vector_base : 沒有對外提供任何函式介面,類內做的就是配合boost::mpl超程式設計庫在編譯期解析模板引數,同時生成boost::totally_ordered1供small_vector繼承,精簡operator程式碼。
  • IntegralSizePolicyBase:負責size/extern/heapifiedCapacity相關的操作。
  • IntegralSizePolicy : 負責內聯資料溢位到堆的過程。

small_vector

宣告:

template <
    class Value,
    std::size_t RequestedMaxInline = 1,
    class PolicyA = void,
    class PolicyB = void,
    class PolicyC = void>
class small_vector : public detail::small_vector_base<
                         Value,
                         RequestedMaxInline,
                         PolicyA,
                         PolicyB,
                         PolicyC>::type 

宣告中有三個策略模板引數是因為在一次提交中刪除了一個無用的策略,OneBitMutex:Delete small_vector's OneBitMutex policy

small_vector_base

template <
    class Value,
    std::size_t RequestedMaxInline,
    class InPolicyA,
    class InPolicyB,
    class InPolicyC>
struct small_vector_base;

boost::mpl放到最後說吧 :)

資料結構

small_vector花了一些心思在capacity的設計上,儘可能減小物件記憶體,降低內聯資料帶來的影響。

union Data負責儲存資料:

union Data{
    PointerType pdata_;          // 溢位到堆後的資料
    InlineStorageType storage_;  // 內聯資料
}u;

InlineStorageType

使用std::aligned_storage進行初始化,佔用空間是sizeof(value_type) * MaxInline,對齊要求為alignof(value_type)

typedef typename std::aligned_storage<sizeof(value_type) * MaxInline, alignof(value_type)>::type InlineStorageType;

capacity

與std::vector用結構體欄位表示capacity不同,small_vector的capacity存放分為三種情況。

capacity內聯在物件中

這是最簡單的一種情況:

image

條件為sizeof(HeapPtrWithCapacity) < sizeof(InlineStorageType) (這裡我不明白為什麼等於的情況不算在內):

static bool constexpr kHasInlineCapacity = sizeof(HeapPtrWithCapacity) < sizeof(InlineStorageType);

typedef typename std::conditional<kHasInlineCapacity, HeapPtrWithCapacity, HeapPtr>::type PointerType;

struct HeapPtrWithCapacity {
  value_type* heap_;
  InternalSizeType capacity_;
} ;

union Data{
    PointerType pdata_;           // 溢位到堆後的資料
    InlineStorageType storage_;   // 內聯資料
}u;

通過malloc_usable_size獲取capacity

假如上述kHasInlineCapacity == false,即sizeof(InlineStorageType) <= sizeof(HeapPtrWithCapacity)時,考慮到節省物件空間,capacity不會內聯在物件中,此時PointerType的型別為HeapPtr,內部只保留一個指標:

struct HeapPtr {
  value_type* heap_;
} ;

union Data{
    PointerType pdata_;           // 溢位到堆後的資料
    InlineStorageType storage_;   // 內聯資料
}u;

那麼此時capacity存放在哪裡了呢?這裡又分了兩種情況,第一種就是這裡要說明的:直接通過malloc_usable_size獲取從堆上分配的記憶體區域的可用資料大小,這個結果就被當做small_vector當前的capacity:

malloc_usable_size(heap_) / sizeof(value_type);    // heap_指向堆上的資料

image

但是有一個問題,由於記憶體分配存在alignment和minimum size constraints的情況,malloc_usable_size返回的大小可能會大於申請時指定的大小,但是folly會利用這部分多餘的空間來存放資料(如果能放的下)。

比如在不使用jemalloc的情況下,在擴容的函式內,將向系統申請的位元組數、malloc_usable_size返回的可用空間、small_vector的capacity列印出來:

folly::small_vector<uint32_t, 2> vec;     // uint32_t => four bytes

for (int i = 0; i < 200; i++) {
    vec.push_back(1);
    std::cout << vec.capacity() << std::endl;
}

// 程式碼進行了簡化
template <typename EmplaceFunc>
  void makeSizeInternal(size_type newSize, bool insert, EmplaceFunc&& emplaceFunc, size_type pos) {
    const auto needBytes = newSize * sizeof(value_type);
    const size_t goodAllocationSizeBytes = goodMallocSize(needBytes);
    const size_t newCapacity = goodAllocationSizeBytes / sizeof(value_type);

    const size_t sizeBytes = newCapacity * sizeof(value_type);
    void* newh = checkedMalloc(sizeBytes);

    std::cout << "sizeBytes:" << sizeBytes << " malloc_usable_size:" << malloc_usable_size(newh) << " "
              << kMustTrackHeapifiedCapacity << std::endl;

    // move元素等操作,略過....
}

// output :
2
2
sizeBytes:16 malloc_usable_size:24 0
6
6
6
6
sizeBytes:40 malloc_usable_size:40 0
10
10
10
10
sizeBytes:64 malloc_usable_size:72 0
18
18
18
18
18
18
18
18
......
......

可以看出,擴容時即使向系統申請16位元組的空間,malloc_usable_size返回的是24位元組,而small_vector此時的capacity也是24,即會利用多餘的8個位元組額外寫入2個資料。

**如果使用了jemalloc **,那麼會根據size classes分配空間。

這種方式也是有使用條件的,即needbytes >= kHeapifyCapacityThreshold,kHeapifyCapacityThreshold的定義為:

// This value should we multiple of word size.
static size_t constexpr kHeapifyCapacitySize = sizeof(typename std::aligned_storage<sizeof(InternalSizeType), alignof(value_type)>::type);

// Threshold to control capacity heapifying.
static size_t constexpr kHeapifyCapacityThreshold = 100 * kHeapifyCapacitySize;

我沒想明白這個100是怎麼定下來的 ?

將capacity放到堆上

當需要申請的記憶體needbytes >= kHeapifyCapacityThreshold時,就會直接將capacity放到堆上進行管理:

image

此時需要多申請sizeof(InternalSizeType)位元組來存放capacity,並且需要對記憶體分配介面返回的指標加上sizeof(InternalSizeType)從而指向真正的資料:

inline void* shiftPointer(void* p, size_t sizeBytes) { 
    return static_cast<char*>(p) + sizeBytes; 
}

template <typename EmplaceFunc>
void makeSizeInternal(size_type newSize, bool insert, EmplaceFunc&& emplaceFunc, size_type pos) {
   ....
    const bool heapifyCapacity = !kHasInlineCapacity && needBytes >= kHeapifyCapacityThreshold;

    // 申請記憶體
    void* newh = checkedMalloc(sizeBytes);
    value_type* newp =
        static_cast<value_type*>(heapifyCapacity ? detail::shiftPointer(newh, kHeapifyCapacitySize) : newh);
    
    // move元素等操作,略過....
    u.pdata_.heap_ = newp;
   // ....
}

size()相關

那麼如何區分資料是內聯在物件中還是溢位到堆上,如何區分上面三種capacity儲存策略呢?

採取的做法是向size借用兩位來區分:

image

  • kExternMask : 資料是否溢位到堆上,相關函式為isExtern/setExtern.
  • kCapacityMask : capacity是否在堆上額外分配了記憶體來管理。相關函式為isHeapifiedCapacity/setHeapifiedCapacity.
template <class SizeType, bool ShouldUseHeap>
struct IntegralSizePolicyBase {
  IntegralSizePolicyBase() : size_(0) {}
 protected:
  static constexpr std::size_t policyMaxSize() { return SizeType(~kClearMask); }

  std::size_t doSize() const { return size_ & ~kClearMask; }

  std::size_t isExtern() const { return kExternMask & size_; }

  void setExtern(bool b) {
    if (b) {
      size_ |= kExternMask;
    } else {
      size_ &= ~kExternMask;
    }
  }

  std::size_t isHeapifiedCapacity() const { return kCapacityMask & size_; }

  void setHeapifiedCapacity(bool b) {
    if (b) {
      size_ |= kCapacityMask;
    } else {
      size_ &= ~kCapacityMask;
    }
  }
  void setSize(std::size_t sz) {
    assert(sz <= policyMaxSize());
    size_ = (kClearMask & size_) | SizeType(sz);
  }

 private:
  // We reserve two most significant bits of size_.
  static SizeType constexpr kExternMask =
      kShouldUseHeap ? SizeType(1) << (sizeof(SizeType) * 8 - 1) : 0;

  static SizeType constexpr kCapacityMask =
      kShouldUseHeap ? SizeType(1) << (sizeof(SizeType) * 8 - 2) : 0;

  SizeType size_;
};

都是很簡單的位運算。只需要注意下policyMaxSize函式,因為向size借了2位,所以最大的size不是SizeType型別的最大值,需要有額外的判斷。

capacity()函式

因為capacity有三種儲存方式,所以需要根據各自情況去獲取:

size_type capacity() const {
  if (this->isExtern()) {
    if (hasCapacity()) {       // 為capacity分配記憶體的情況
      return u.getCapacity();
    }
    return AllocationSize{}(u.pdata_.heap_) / sizeof(value_type);   // 不為capacity分配空間的情況
  }
  return MaxInline;    // 資料內聯的情況
}

資料內聯在物件中

這是最簡單的情況,根據上面說過的isExtern()判斷資料是否內聯,是的話直接返回MaxInline。

這裡的MaxInline還不是上游傳過來的RequestedMaxInline。因為不管是什麼capacity儲存策略,union Data必然會有一個有指標,最小也是sizeof(void*),假如使用者傳的是small_vector<uint8_t,1>,會替使用者修改MaxInine = 8,最大限度利用物件空間:

/*
  * Figure out the max number of elements we should inline.  (If
  * the user asks for less inlined elements than we can fit unioned
  * into our value_type*, we will inline more than they asked.)
  */
static constexpr std::size_t MaxInline{
    constexpr_max(sizeof(Value*) / sizeof(Value), RequestedMaxInline)};

為capacity分配了記憶體

這裡包括capacity分配的記憶體在堆上或者內聯在物件中。通過hasCapacity()判斷,isHeapifiedCapacity上面說過:

bool hasCapacity() const {
  return kHasInlineCapacity || this->isHeapifiedCapacity();
}

如果hasCapacity()為true,則呼叫u.getCapacity(),可以猜到這個方法呼叫PointerType(HeapPtr/HeapPtrWithCapacity)對應的getCapacity()方法。

union Data {
    PointerType pdata_;
    InlineStorageType storage_;

    InternalSizeType getCapacity() const { return pdata_.getCapacity(); }
  } u;
};

inline void* unshiftPointer(void* p, size_t sizeBytes) {
  return static_cast<char*>(p) - sizeBytes;
}

struct HeapPtr {
  // heap[-kHeapifyCapacitySize] contains capacity
  value_type* heap_;

  InternalSizeType getCapacity() const {
    return *static_cast<InternalSizeType*>(
        detail::unshiftPointer(heap_, kHeapifyCapacitySize));
  }
};

struct HeapPtrWithCapacity {
  value_type* heap_;
  InternalSizeType capacity_;

  InternalSizeType getCapacity() const { return capacity_; }
};

注意unshiftPointer是shiftPointer的反過程,將指標從指向真正的資料回退到指向capacity。

不為capacity分配空間

即通過malloc_usable_size獲取capacity。AllocationSize{}(u.pdata_.heap_)/sizeof(value_type);直接理解為malloc_usable_size(u.pdata_.heap_)/sizeof(value_type)即可,加AllocationSize是為了解決一個Android not having malloc_usable_size below API17,不是重點。

size_type capacity() const {
  if (this->isExtern()) {
    if (hasCapacity()) {
      return u.getCapacity();
    }
    return AllocationSize{}(u.pdata_.heap_) / sizeof(value_type);   // 此種場景
  }
  return MaxInline;
}

擴容

與std::vector擴容的不同點:

  • small_vector根據模板引數是否滿足is_trivially_copyable進行了不同的實現:
  • std::vector遷移元素時,會根據是否有noexcept move constructor來決定呼叫move constructor還是copy constructor(之前這篇文章提到過:c++ 從vector擴容看noexcept應用場景)。但small_vector沒有這個過程,有move constructor就直接呼叫,不判斷是否有noexcept。所以,當呼叫move constructor有異常時,原有記憶體區域的資料會被破壞
  • 擴容因子不同。std::vector一般為2或者1.5,不同平臺不一樣。small_vector的capacity上面已經提到過。

最終擴容流程都會走到moveToUninitialized函式。

/*
  * Move a range to a range of uninitialized memory.  Assumes the
  * ranges don't overlap.
  */
template <class T>
typename std::enable_if<!folly::is_trivially_copyable<T>::value>::type moveToUninitialized(T* first, T* last, T* out) {
  std::size_t idx = 0;
  {
    auto rollback = makeGuard([&] {
      for (std::size_t i = 0; i < idx; ++i) {
        out[i].~T();
      }
    });
    for (; first != last; ++first, ++idx) {
      new (&out[idx]) T(std::move(*first));
    }
    rollback.dismiss();
  }
}

// Specialization for trivially copyable types.
template <class T>
typename std::enable_if<folly::is_trivially_copyable<T>::value>::type moveToUninitialized(T* first, T* last, T* out) {
  std::memmove(static_cast<void*>(out), static_cast<void const*>(first), (last - first) * sizeof *first);
}

這裡我不太理解的一點是,既然註釋中提到假設前後記憶體沒有overlap,那麼在is_trivially_copyable的版本中為什麼不用std::memcpy呢?效率還能高一點。

先遷移原有資料還是先放入新資料

在之前的版本中,流程是申請新記憶體、遷移原有資料、放入新資料:

template<class ...Args>
void emplaceBack(Args&&... args) {
  makeSize(size() + 1);        // 申請新記憶體 + 遷移原有資料,內部會呼叫上面的moveToUninitialized
  new (end()) value_type(std::forward<Args>(args)...);    //放入新資料
  this->setSize(size() + 1);
}

small_vector improvements提交中,改為了申請新記憶體、放入新資料、遷移原有資料。理由是有可能emplace_back的資料是small_vector中的某一個資料的引用, 比如這次提交中加的ForwardingEmplaceInsideVector test,不過這屬於corner case。

makeGuard

Support -fno-exceptions in folly/small_vector.h提交裡,使用makeGuard代替了原有的try-catch。makeGuard定義在folly/ScopeGuard.h中。

之前try-catch版本:

template <class T>
typename std::enable_if<!folly::is_trivially_copyable<T>::value>::type
moveToUninitialized(T* first, T* last, T* out) {
  std::size_t idx = 0;
  try {
    for (; first != last; ++first, ++idx) {
      new (&out[idx]) T(std::move(*first));
    }
  } catch (...) {
    for (std::size_t i = 0; i < idx; ++i) {
      out[i].~T();
    }
    throw;
  }
}

可以對比上面的makeGuard版本,邏輯沒有變化。

Andrei Alexandrescu和Petru Marginean在2000年寫的Generic: Change the Way You Write Exception-Safe Code — Forever中,介紹了編寫Exception-Safe Code常見的方法並提出了ScopeGuard的概念:

  • 什麼都不加,戰略上藐視敵人,戰術上也藐視敵人。
  • try-catch : 最常見,缺點是:異常路徑效能損失較大(stack-unwinding)、編譯後的二進位制檔案體積膨脹等。
  • RAII : 需要針對每一種異常寫一個小類去處理,繁瑣。
  • scopeGuard : 上面提到的用法,也是利用了RAII的思想,但是更加通用。應用程式只需要定義rollback,並在成功後呼叫dismiss即可。

其他

clear優化

optimize small_vector::clear提交中,利用了Clang/GCC對clear函式進行了優化,生成更少的彙編指令(這都是怎麼發現的???):

// 優化前
void clear() {
  erase(begin(), end());
}

// 優化後
void clear() {
  // Equivalent to erase(begin(), end()), but neither Clang or GCC are able to optimize away the abstraction.
    for (auto it = begin(); it != end(); ++it) {
      it->~value_type();
    }
    this->setSize(0);
}

image

__attribute__((__pack__))/pragma(pack(push, x))

folly中包裝了編譯器對齊係數,相關引數介紹 : C/C++記憶體對齊詳解

// packing is very ugly in msvc
#ifdef _MSC_VER
#define FOLLY_PACK_ATTR /**/
#define FOLLY_PACK_PUSH __pragma(pack(push, 1))
#define FOLLY_PACK_POP __pragma(pack(pop))
#elif defined(__GNUC__)
#define FOLLY_PACK_ATTR __attribute__((__packed__))
#define FOLLY_PACK_PUSH /**/
#define FOLLY_PACK_POP /**/
#else
#define FOLLY_PACK_ATTR /**/
#define FOLLY_PACK_PUSH /**/
#define FOLLY_PACK_POP /**/
#endif

Fix alignment issues in small_vector提交中,small_vector改為了只在64位平臺使用對齊係數(不知道為什麼,哭。。。):

#if (FOLLY_X64 || FOLLY_PPC64)
#define FOLLY_SV_PACK_ATTR FOLLY_PACK_ATTR
#define FOLLY_SV_PACK_PUSH FOLLY_PACK_PUSH
#define FOLLY_SV_PACK_POP FOLLY_PACK_POP
#else
#define FOLLY_SV_PACK_ATTR
#define FOLLY_SV_PACK_PUSH
#define FOLLY_SV_PACK_POP
#endif

boost::mpl超程式設計庫

boost mpl文件

small_vector_base的程式碼很少,關注下boost::mpl的使用:

namespace mpl = boost::mpl;

template <
    class Value,
    std::size_t RequestedMaxInline,
    class InPolicyA,
    class InPolicyB,
    class InPolicyC>
struct small_vector_base {

  typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList;

  /*
   * Determine the size type
   */
  typedef typename mpl::filter_view<
      PolicyList,
      std::is_integral<mpl::placeholders::_1>>::type Integrals;
  typedef typename mpl::eval_if<
      mpl::empty<Integrals>,
      mpl::identity<std::size_t>,
      mpl::front<Integrals>>::type SizeType;

  /*
   * Determine whether we should allow spilling to the heap or not.
   */
  typedef typename mpl::count<PolicyList, small_vector_policy::NoHeap>::type
      HasNoHeap;

  /*
   * Make the real policy base classes.
   */
  typedef IntegralSizePolicy<SizeType, !HasNoHeap::value> ActualSizePolicy;

  /*
   * Now inherit from them all.  This is done in such a convoluted
   * way to make sure we get the empty base optimizaton on all these
   * types to keep sizeof(small_vector<>) minimal.
   */
  typedef boost::totally_ordered1<
      small_vector<Value, RequestedMaxInline, InPolicyA, InPolicyB, InPolicyC>,
      ActualSizePolicy>
      type;
};

解析模板引數的思路為:

  • typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList; : 將策略放入到mpl::vector中,例如Noheap、指定size的資料型別(uint32_t、uint16_t等)

接下來可以分為兩塊,獲取SizeType和獲取HasNoHeap:

獲取SizeType:

  • typedef typename mpl::filter_view<PolicyList, std::is_integral<mpl::placeholders::_1>>::type Integrals; : 將符合std::is_integral條件的篩選出來,比如uint8_t。
    • mpl::filter_view被定義為template< typename Sequence, typename Pred>,其中,Pred需要是Unary Lambda Expression,即為編譯期可呼叫的entity,分為Metafunction ClassPlaceholder Expression,上面的std::is_integral<mpl::placeholders::_1>即為Placeholder Expression
  • typedef typename mpl::eval_if<mpl::empty<Integrals>, mpl::identity<std::size_t>, mpl::front<Integrals>>::type SizeType;不用知道每個函式的意思,從字面也能看出來:假如應用層傳入的模板引數沒有std::is_integral,那麼SizeType,即size的型別,就是size_t,否則就是應用傳入的型別,比如uint8_t.

獲取HasNoHeap:

  • typedef typename mpl::count<PolicyList, small_vector_policy::NoHeap>::type HasNoHeap;,這個也能猜出來:應用層是否指定了NoHeap.

image

可以看出,解析模板引數並沒有依賴引數的特定順序。

boost/operators

boost/operators文件參考 : Header <boost/operators.hpp>

small_vector_base的最後兩行程式碼,與boost/operators有關 :

typedef IntegralSizePolicy<SizeType, !HasNoHeap::value> ActualSizePolicy;

typedef boost::totally_ordered1<
    small_vector<Value, RequestedMaxInline, InPolicyA, InPolicyB, InPolicyC>,
    ActualSizePolicy>
    type;

boost/operators為精簡operator程式碼而設計,比如我們想要支援x < y,那麼x > y、x >= y、和x <= y同樣也需要。但是理想情況下,我們只需要過載operator<就行了,後面三個操作可以根據x<y推匯出來,boost/operators可以為我們生成類似下面的程式碼:

bool operator>(const T& x, const T& y)  { return y < x; }
bool operator<=(const T& x, const T& y) { return !static_cast<bool>(y < x); }
bool operator>=(const T& x, const T& y) { return !static_cast<bool>(x < y); }

boost::totally_ordered1被small_vector繼承。按照boost::totally_ordered1的定義,只要實現operator<operator=,那麼除了這兩個操作,boost/operators也會自動生成operator>operator<=operator>=operator!=

// class small_vector

bool operator==(small_vector const& o) const {
    return size() == o.size() && std::equal(begin(), end(), o.begin());
}

bool operator<(small_vector const& o) const {
    return std::lexicographical_compare(begin(), end(), o.begin(), o.end());
}

對比std::vector,在c++20之前,std::vector實現了所有的operator==operator!=operator<operator<=operator>operator>=,比較繁瑣。

c++20的<=> (three-way comparison)

c++20提供了一個新特性,three-way comparison,可以提供上面boost/operators的功能。可以參考:

比如,std::vector在c++20後,就廢棄了<、<=、 >=、!= operators (下面引用來自於operator==,!=,<,<=,>,>=,<=>(std::vector)
):

The <, <=, >, >=, and != operators are synthesized from operator<=> and operator== respectively. (since C++20)

(完)

朋友們可以關注下我的公眾號,獲得最及時的更新:

相關文章