← Back to Blog
C++InterviewC++26Memory
面試被要求手寫 C++26 inplace_vector
2026-03-31
世界頂級量化交易公司的 C++ 面試,面試官劍橋畢業,要求現場從零實作一個 inplace_vector。 不能用 STL container,自己管理記憶體、物件生命週期、alignment。
什麼是 inplace_vector?
std::inplace_vector<T, N> 是 C++26 新增的 container, 你可以把它想像成 固定容量的 vector:
| 特性 | std::vector | std::array | inplace_vector |
|---|---|---|---|
| 大小 | 動態 | 固定 N | 動態,最大 N |
| 記憶體 | Heap | Inline | Inline |
| Heap alloc | 會 | 不會 | 不會 |
| 元素初始化 | push 時才建構 | 全部預先建構 | push 時才建構 |
text
inplace_vector<int, 4> 的記憶體 layout:
物件本身(全部在 stack 上):┌────────┬─────┬─────┬─────┬─────┐│ size=2 │ 10 │ 20 │ ? │ ? ││ (8B) │ [0] │ [1] │ --- │ --- │└────────┴─────┴─────┴─────┴─────┘ ↑ 已建構 ↑ 未初始化的空間
對比 std::vector<int>:┌──────────┬──────┬──────────┐│ ptr(8B) │ size │ capacity │ ──→ [10, 20] (heap)└──────────┴──────┴──────────┘
重點:inplace_vector 的資料就在物件內部,不碰 heap。在 HFT 場景中,不 malloc = 不會觸發 latency spike。面試要求:從零手刻一個 inplace_vector,支援 push_back、emplace_back、pop_back、 operator[]、size、capacity、begin/end。不能用任何 STL container。
Step 1 - 記憶體 Layout:aligned storage
第一個挑戰:如何在物件內部準備一塊「未初始化但正確對齊」的記憶體?
為什麼不能用 T data[N]?
cpp
// ✗ 錯誤:array 會對所有 N 個元素呼叫 default constructortemplate <typename T, size_t N>struct bad_inplace_vector { T data[N]; // 如果 T 沒有 default constructor → 編譯失敗 size_t sz; // 如果 T 的 constructor 有副作用 → 行為錯誤};inplace_vector 的語意是「push 的時候才建構」,所以我們需要一塊 raw memory, 不觸發任何 constructor,等之後用 placement new 手動建構物件。
正確做法:alignas + std::byte
cpp
template <typename T, size_t N>class inplace_vector { alignas(T) std::byte storage_[sizeof(T) * N]; size_t size_ = 0;
// 取得第 i 個 slot 的指標 T* ptr(size_t i) noexcept { return std::launder( reinterpret_cast<T*>(storage_ + sizeof(T) * i) ); } const T* ptr(size_t i) const noexcept { return std::launder( reinterpret_cast<const T*>(storage_ + sizeof(T) * i) ); }};alignas(T)
確保 storage_ 的起始位址滿足 T 的對齊要求。 如果 T 是
double(alignof = 8),storage_ 的位址一定是 8 的倍數。 少了這個,在某些架構上會 bus fault。std::launder
C++17 引入。reinterpret_cast 後的 pointer 在嚴格 aliasing 規則下可能不合法,
std::launder 告訴 compiler「這個位址上已經有一個 T 物件了,相信我」。 面試時提到這個會加分,但不是所有 interviewer 都會要求。Step 2 - 物件生命週期
raw memory 上建構物件用 placement new,銷毀用 explicit destructor call。 這是 C++ object model中最底層的操作:
cpp
// 在 storage_ 的第 i 個 slot 上建構物件template <typename... Args>void construct_at(size_t i, Args&&... args) { ::new (static_cast<void*>(storage_ + sizeof(T) * i)) T(std::forward<Args>(args)...);}
// 銷毀第 i 個 slot 上的物件(不釋放記憶體)void destroy_at(size_t i) { ptr(i)->~T();}text
placement new 做的事:
普通 new: 1. malloc 一塊記憶體 ← operator new 2. 在上面呼叫 constructor ← placement
placement new(跳過 step 1): ::new (addr) T(args...); ↑ 不 allocate,直接在 addr 上呼叫 constructor
explicit destructor call: ptr->~T(); ↑ 只呼叫 destructor,不 free 記憶體(記憶體是 storage_ 的一部分)這組操作在 C++20 後也可以用std::construct_at和std::destroy_at(在 <memory> 裡)。面試時兩種寫法都可以, 但手寫 placement new 展示你理解底層。
Step 3 - 核心操作
cpp
// push_back - copy versionvoid push_back(const T& value) { if (size_ >= N) throw std::bad_alloc{}; construct_at(size_, value); ++size_;}
// push_back - move versionvoid push_back(T&& value) { if (size_ >= N) throw std::bad_alloc{}; construct_at(size_, std::move(value)); ++size_;}
// emplace_back - perfect forwardingtemplate <typename... Args>T& emplace_back(Args&&... args) { if (size_ >= N) throw std::bad_alloc{}; construct_at(size_, std::forward<Args>(args)...); return *ptr(size_++); // 注意:這裡 size_++ 要在 construct 之後, // 如果 constructor throw,size_ 不變 → strong exception guarantee}
// pop_backvoid pop_back() { assert(size_ > 0); --size_; destroy_at(size_);}
// element accessT& operator[](size_t i) { return *ptr(i); }const T& operator[](size_t i) const { return *ptr(i); }
// size & capacitysize_t size() const noexcept { return size_; }static constexpr size_t capacity() noexcept { return N; }bool empty() const noexcept { return size_ == 0; }
// iteratorsT* begin() noexcept { return ptr(0); }T* end() noexcept { return ptr(size_); }const T* begin() const noexcept { return ptr(0); }const T* end() const noexcept { return ptr(size_); }Pitfall
emplace_back 的 size 更新順序很重要。 如果先
++size_ 再 construct, constructor throw 時 size_ 已經增加了,但物件沒有建構成功 - destructor 會試圖銷毀一個不存在的物件 = UB。 正確順序:先 construct,成功後才 ++size_。Pitfall
capacity() 是 static constexpr。 容量在編譯期就確定了,不是 runtime 值。 這是面試小亮點 - 展示你理解 inplace_vector 和 vector 的本質區別。
Step 4 - Rule of Five
因為我們手動管理物件生命週期,compiler 生成的 default copy/move 是 bitwise copy storage_ - 這對 non-trivial type 是 UB。必須手寫 Rule of Five:
cpp
// Destructor - 銷毀所有已建構的元素~inplace_vector() { clear();}
void clear() noexcept { while (size_ > 0) { pop_back(); }}
// Copy constructorinplace_vector(const inplace_vector& other) : size_(0) { for (size_t i = 0; i < other.size_; ++i) { construct_at(i, *other.ptr(i)); ++size_; // 逐個增加,exception safety }}
// Move constructorinplace_vector(inplace_vector&& other) noexcept( std::is_nothrow_move_constructible_v<T>) : size_(0) { for (size_t i = 0; i < other.size_; ++i) { construct_at(i, std::move(*other.ptr(i))); ++size_; } other.clear();}
// Copy assignmentinplace_vector& operator=(const inplace_vector& other) { if (this == &other) return *this; clear(); for (size_t i = 0; i < other.size_; ++i) { construct_at(i, *other.ptr(i)); ++size_; } return *this;}
// Move assignmentinplace_vector& operator=(inplace_vector&& other) noexcept( std::is_nothrow_move_constructible_v<T>) { if (this == &other) return *this; clear(); for (size_t i = 0; i < other.size_; ++i) { construct_at(i, std::move(*other.ptr(i))); ++size_; } other.clear(); return *this;}為什麼 move constructor 裡要逐個 ++size_?
如果第 3 個元素的 move constructor 丟 exception,前 2 個已經建構好的物件 需要被正確銷毀。逐個增加 size_ 確保 destructor 只銷毀已建構的部分。 這是 strong exception guarantee。
noexcept 條件式
move constructor 的 noexcept 取決於 T 的 move constructor。 如果 T 的 move 不是 noexcept(例如某些自訂type), 我們的 inplace_vector 的 move 也不能是 noexcept。 這影響
std::vector<inplace_vector> 在 realloc 時會用 move 還是 copy。完整實作
cpp
#include <cstddef>#include <cassert>#include <new>#include <type_traits>#include <utility>#include <memory> // std::launder
template <typename T, size_t N>class inplace_vector { alignas(T) std::byte storage_[sizeof(T) * N]; size_t size_ = 0;
T* ptr(size_t i) noexcept { return std::launder(reinterpret_cast<T*>( storage_ + sizeof(T) * i)); } const T* ptr(size_t i) const noexcept { return std::launder(reinterpret_cast<const T*>( storage_ + sizeof(T) * i)); }
template <typename... Args> void construct_at_(size_t i, Args&&... args) { ::new (static_cast<void*>(storage_ + sizeof(T) * i)) T(std::forward<Args>(args)...); }
void destroy_at_(size_t i) noexcept { ptr(i)->~T(); }
public: // --- constructors / destructor ---
inplace_vector() = default;
inplace_vector(const inplace_vector& other) : size_(0) { for (size_t i = 0; i < other.size_; ++i) { construct_at_(i, *other.ptr(i)); ++size_; } }
inplace_vector(inplace_vector&& other) noexcept(std::is_nothrow_move_constructible_v<T>) : size_(0) { for (size_t i = 0; i < other.size_; ++i) { construct_at_(i, std::move(*other.ptr(i))); ++size_; } other.clear(); }
~inplace_vector() { clear(); }
// --- assignment ---
inplace_vector& operator=(const inplace_vector& other) { if (this != &other) { clear(); for (size_t i = 0; i < other.size_; ++i) { construct_at_(i, *other.ptr(i)); ++size_; } } return *this; }
inplace_vector& operator=(inplace_vector&& other) noexcept(std::is_nothrow_move_constructible_v<T>) { if (this != &other) { clear(); for (size_t i = 0; i < other.size_; ++i) { construct_at_(i, std::move(*other.ptr(i))); ++size_; } other.clear(); } return *this; }
// --- modifiers ---
void push_back(const T& value) { if (size_ >= N) throw std::bad_alloc{}; construct_at_(size_, value); ++size_; }
void push_back(T&& value) { if (size_ >= N) throw std::bad_alloc{}; construct_at_(size_, std::move(value)); ++size_; }
template <typename... Args> T& emplace_back(Args&&... args) { if (size_ >= N) throw std::bad_alloc{}; construct_at_(size_, std::forward<Args>(args)...); return *ptr(size_++); }
void pop_back() noexcept { assert(size_ > 0); --size_; destroy_at_(size_); }
void clear() noexcept { while (size_ > 0) pop_back(); }
// --- access ---
T& operator[](size_t i) noexcept { return *ptr(i); } const T& operator[](size_t i) const noexcept { return *ptr(i); }
T& front() noexcept { return *ptr(0); } T& back() noexcept { return *ptr(size_ - 1); }
// --- capacity ---
size_t size() const noexcept { return size_; } static constexpr size_t capacity() noexcept { return N; } bool empty() const noexcept { return size_ == 0; } bool full() const noexcept { return size_ == N; }
// --- iterators ---
T* begin() noexcept { return ptr(0); } T* end() noexcept { return ptr(size_); } const T* begin() const noexcept { return ptr(0); } const T* end() const noexcept { return ptr(size_); } T* data() noexcept { return ptr(0); }};用法
cpp
#include <iostream>#include <string>
int main() { inplace_vector<std::string, 4> v;
v.push_back("hello"); v.emplace_back("world"); v.push_back(std::string(50, 'x')); // 長字串,但 vector 本身不 malloc
std::cout << "size: " << v.size() << "\n"; // 3 std::cout << "capacity: " << v.capacity() << "\n"; // 4
for (const auto& s : v) { std::cout << s << "\n"; }
// sizeof 是固定的,不管放了幾個元素 std::cout << "sizeof: " << sizeof(v) << "\n"; // = alignof(string) padding + sizeof(string) * 4 + sizeof(size_t)
v.pop_back(); // 銷毀 "xxx...",string 的 destructor 會 free heap v.clear(); // 銷毀剩餘元素}注意:inplace_vector 本身不做 heap allocation,但 T 本身可能會。 例如 inplace_vector<std::string, N> 裡如果放長字串, string 的 data 仍然在 heap 上(除非走 SSO)。 inplace_vector 保證的是「container 自身不 malloc」,不是「整體不 malloc」。面試常見踩坑點
Pitfall
1. 忘記 alignas
直接用
直接用
char storage_[sizeof(T) * N], 如果 T 的 alignment 大於 1(幾乎所有type),就是 UB。 面試官會直接問「你的 buffer 對齊了嗎?」Pitfall
2. 用 T data[N] 而不是 raw storage
會強制所有元素被 default construct。如果 T 沒有 default constructor 就編譯不過, 如果有的話也違反了 inplace_vector 的語意(size = 0 時不該有任何 T 物件存在)。
會強制所有元素被 default construct。如果 T 沒有 default constructor 就編譯不過, 如果有的話也違反了 inplace_vector 的語意(size = 0 時不該有任何 T 物件存在)。
Pitfall
3. Destructor 裡忘記銷毀元素
default destructor 不會幫你呼叫每個 T 的 destructor - 因為 compiler 看到的只是
default destructor 不會幫你呼叫每個 T 的 destructor - 因為 compiler 看到的只是
std::byte[],不知道上面有活著的 T 物件。記憶體洩漏 + resource leak。Pitfall
4. Copy/Move 用 memcpy
對 trivially copyable types(int, double)可以用 memcpy, 但對 non-trivial types(string, unique_ptr)是 UB。 面試時安全做法是逐個元素 copy/move construct。
進階優化:可以用
對 trivially copyable types(int, double)可以用 memcpy, 但對 non-trivial types(string, unique_ptr)是 UB。 面試時安全做法是逐個元素 copy/move construct。
進階優化:可以用
if constexpr (std::is_trivially_copyable_v<T>)做 branch,trivial 用 memcpy,non-trivial 逐個 construct。Pitfall
5. Exception safety 沒處理
constructor throw 時,已經建構好的元素要回滾銷毀。 面試不一定要求你寫 try-catch,但至少要口頭提到這個問題。
constructor throw 時,已經建構好的元素要回滾銷毀。 面試不一定要求你寫 try-catch,但至少要口頭提到這個問題。
Pitfall
6. emplace_back 的 size 更新時機
先
先
++size_ 再 construct:如果 constructor throw, destructor 會嘗試銷毀不存在的物件 → UB。 正確順序永遠是 construct first, then update size。總結
| 考點 | 面試官想看到 |
|---|---|
| Memory layout | alignas + raw byte array,不是 T[] |
| Object lifetime | placement new construct, explicit dtor destroy |
| Rule of Five | 手寫 copy/move ctor & assignment, dtor clear |
| Exception safety | construct before ++size_, destructor 只銷毀已建構的 |
| Move semantics | conditional noexcept, forward, move 已建構的元素 |
| Template | perfect forwarding (emplace_back), constexpr capacity |
面試心得
老實說面試當下基本上寫不出來,placement new、alignment 這些平常根本不會碰到, 當場被問直接傻住。
但也因為這次被電得很慘,後來才花時間把 inplace_vector 從頭到尾搞懂, 整理成這篇文章。結果學到的東西比面試過了還多。
C++C++26Interviewinplace_vectorPlacement NewMemoryHFT