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


介紹

行為與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)

(完)

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM