面試手刻 C++26 inplace_vector
2026-03-31
某頂級量化交易公司的 C++ 面試,要求現場從零實作一個 inplace_vector。 不能用 STL container,要自己管理記憶體、物件生命週期、alignment。 這題考的不是演算法,是你對 C++ 物件模型的理解有多深。
什麼是 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 時才建構 |
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]?
// ✗ 錯誤:array 會對所有 N 個元素呼叫 default constructor
template <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
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)
double(alignof = 8),storage_ 的位址一定是 8 的倍數。 少了這個,在某些架構上會 bus fault。std::launder
std::launder 告訴 compiler「這個位址上已經有一個 T 物件了,相信我」。 面試時提到這個會加分,但不是所有 interviewer 都會要求。Step 2 - 物件生命週期
raw memory 上建構物件用 placement new,銷毀用 explicit destructor call。 這是 C++ 物件模型中最底層的操作:
// 在 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();
}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 - 核心操作
// push_back - copy version
void push_back(const T& value) {
if (size_ >= N) throw std::bad_alloc{};
construct_at(size_, value);
++size_;
}
// push_back - move version
void push_back(T&& value) {
if (size_ >= N) throw std::bad_alloc{};
construct_at(size_, std::move(value));
++size_;
}
// emplace_back - perfect forwarding
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_++);
// 注意:這裡 size_++ 要在 construct 之後,
// 如果 constructor throw,size_ 不變 → strong exception guarantee
}
// pop_back
void pop_back() {
assert(size_ > 0);
--size_;
destroy_at(size_);
}
// element access
T& operator[](size_t i) { return *ptr(i); }
const T& operator[](size_t i) const { return *ptr(i); }
// size & capacity
size_t size() const noexcept { return size_; }
static constexpr size_t capacity() noexcept { return N; }
bool empty() const noexcept { return size_ == 0; }
// 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_); }++size_ 再 construct, constructor throw 時 size_ 已經增加了,但物件沒有建構成功 - destructor 會試圖銷毀一個不存在的物件 = UB。 正確順序:先 construct,成功後才 ++size_。Step 4 - Rule of Five
因為我們手動管理物件生命週期,compiler 生成的 default copy/move 是 bitwise copy storage_ - 這對 non-trivial type 是 UB。必須手寫 Rule of Five:
// Destructor - 銷毀所有已建構的元素
~inplace_vector() {
clear();
}
void clear() noexcept {
while (size_ > 0) {
pop_back();
}
}
// Copy constructor
inplace_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 constructor
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();
}
// Copy assignment
inplace_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 assignment
inplace_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_?
noexcept 條件式
std::vector<inplace_vector> 在 realloc 時會用 move 還是 copy。完整實作
#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); }
};用法
#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」。面試常見踩坑點
直接用
char storage_[sizeof(T) * N], 如果 T 的 alignment 大於 1(幾乎所有型別),就是 UB。 面試官會直接問「你的 buffer 對齊了嗎?」會強制所有元素被 default construct。如果 T 沒有 default constructor 就編譯不過, 如果有的話也違反了 inplace_vector 的語意(size = 0 時不該有任何 T 物件存在)。
default destructor 不會幫你呼叫每個 T 的 destructor - 因為 compiler 看到的只是
std::byte[],不知道上面有活著的 T 物件。記憶體洩漏 + resource leak。對 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。constructor throw 時,已經建構好的元素要回滾銷毀。 面試不一定要求你寫 try-catch,但至少要口頭提到這個問題。
先
++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 |
面試心得
這題不是考你能不能刷 LeetCode,而是考你真的懂不懂 C++ 的物件模型。 placement new、alignment、explicit destructor call - 這些是每天寫 vector.push_back() 時背後在跑的東西。 量化交易公司在意的是:你能不能在沒有 STL 的情況下, 用最底層的工具正確地管理記憶體和物件。