← Back to Blog
C++InterviewC++26Memory

面試手刻 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::vectorstd::arrayinplace_vector
大小動態固定 N動態,最大 N
記憶體HeapInlineInline
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 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

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++ 物件模型中最底層的操作:

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_atstd::destroy_at(在 <memory> 裡)。面試時兩種寫法都可以, 但手寫 placement new 展示你理解底層。

Step 3 - 核心操作

cpp
// 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_); }
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 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_?

如果第 3 個元素的 move constructor 丟 exception,前 2 個已經建構好的物件 需要被正確銷毀。逐個增加 size_ 確保 destructor 只銷毀已建構的部分。 這是 strong exception guarantee

noexcept 條件式

move constructor 的 noexcept 取決於 T 的 move constructor。 如果 T 的 move 不是 noexcept(例如某些自訂型別), 我們的 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(幾乎所有型別),就是 UB。 面試官會直接問「你的 buffer 對齊了嗎?」
Pitfall
2. 用 T data[N] 而不是 raw storage
會強制所有元素被 default construct。如果 T 沒有 default constructor 就編譯不過, 如果有的話也違反了 inplace_vector 的語意(size = 0 時不該有任何 T 物件存在)。
Pitfall
3. Destructor 裡忘記銷毀元素
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。
進階優化:可以用 if constexpr (std::is_trivially_copyable_v<T>)做 branch,trivial 用 memcpy,non-trivial 逐個 construct。
Pitfall
5. Exception safety 沒處理
constructor throw 時,已經建構好的元素要回滾銷毀。 面試不一定要求你寫 try-catch,但至少要口頭提到這個問題。
Pitfall
6. emplace_back 的 size 更新時機
++size_ 再 construct:如果 constructor throw, destructor 會嘗試銷毀不存在的物件 → UB。 正確順序永遠是 construct first, then update size。

總結

考點面試官想看到
Memory layoutalignas + raw byte array,不是 T[]
Object lifetimeplacement new construct, explicit dtor destroy
Rule of Five手寫 copy/move ctor & assignment, dtor clear
Exception safetyconstruct before ++size_, destructor 只銷毀已建構的
Move semanticsconditional noexcept, forward, move 已建構的元素
Templateperfect forwarding (emplace_back), constexpr capacity

面試心得

這題不是考你能不能刷 LeetCode,而是考你真的懂不懂 C++ 的物件模型。 placement new、alignment、explicit destructor call - 這些是每天寫 vector.push_back() 時背後在跑的東西。 量化交易公司在意的是:你能不能在沒有 STL 的情況下, 用最底層的工具正確地管理記憶體和物件。

C++C++26Interviewinplace_vectorPlacement NewMemoryHFT