ジュニア C++ 面接対策

ジュニア C++ Backend 面接質問

言語の基礎と基本的な所有権を明確に説明する必要がある Backend Developer 向けに、ジュニア C++ の面接質問を 15 問厳選しました。

ジュニア C++ の AI 面接を始めるクレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

Resource Management

1RAII (Resource Acquisition Is Initialization) とは何か、またそれがC++バックエンドサービスにおける例外安全なリソース管理にどのように寄与するかを説明してください。

RAII (Resource Acquisition Is Initialization) とは、C++オブジェクトがリソースを所有し、自身のデストラクタからそのリソースを解放する設計パターンです。ローカルオブジェクトは、例外によるスタック巻き戻し(stack unwinding)を含め、その寿命やスコープが終了した際に自動的に破棄されるため、RAIIによって確定的なクリーンアップが実現され、エラー発生時の処理経路が例外安全になります。バックエンドサービスでは、これはメモリだけでなく、ファイルディスクリプタ、ソケット、ミューテックスのロック、データベースハンドル、トランザクション、その他OSやアプリケーションのリソース全般にも適用されます。

class Fd {
public:
    explicit Fd(int fd = -1) noexcept : fd_(fd) {}
    ~Fd() { if (fd_ != -1) ::close(fd_); }

    Fd(const Fd&) = delete;
    Fd& operator=(const Fd&) = delete;

    Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
    Fd& operator=(Fd&& other) noexcept {
        if (this != &other) {
            if (fd_ != -1) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }

private:
    int fd_;
};

void handle_request() {
    Fd sock(::accept(...));
    if (sock.get() == -1) throw std::runtime_error("accept failed");
    // If any later operation throws, sock is still closed by ~Fd().
}
AI コーチを使ってこの質問に答えてみる

メモリ管理

2std::unique_ptr と std::shared_ptr を比較し、バックエンドAPIにおいてそれぞれがどのような場面で適切であるかを説明してください。

std::unique_ptr は排他的所有権を表します。軽量でムーブ可能ですがコピーはできず、単一の所有者が存在する場合や、所有権を譲渡するAPIに適しています。std::shared_ptr は共有所有権を表します。コピー可能であり、最後の強参照所有者が解放するまで参照カウントによってオブジェクトの寿命を維持します。バックエンドAPIでは、所有権を譲渡する場合は unique_ptr を使用し、独立した複数の所有者がオブジェクトの寿命を延長する必要がある場合にのみ shared_ptr を使用し、所有権を持たないアクセスには参照や生ポインタを使用します。効率性と例外安全性の観点から、shared_ptr オブジェクトを生成する際は通常 std::make_shared を使用することが推奨されます。

struct Session {};

std::unique_ptr<Session> create_session();      // caller receives ownership
void process(Session& s);                       // borrows, does not own
void maybe_process(const Session* s);           // nullable borrow

class Dispatcher {
public:
    void add(std::shared_ptr<Session> s) {      // Dispatcher shares lifetime
        sessions_.push_back(std::move(s));
    }
private:
    std::vector<std::shared_ptr<Session>> sessions_;
};
AI コーチを使ってこの質問に答えてみる

型システム

3ムーブセマンティクス(move semantics)とは何ですか。また、適切なムーブコンストラクタとムーブ代入演算子はどのように実装しますか。

ムーブセマンティクスとは、一時オブジェクトなど不要になったオブジェクトから、コピーではなくリソースの所有権を移動(転送)させる C++ の仕組みです。T&& のような右辺値参照や、ムーブ用オーバーロードの選択を可能にするキャスト関数である std::move を利用します(std::move 自体がリソースを移動するわけではありません)。適切なムーブコンストラクタは、移動元オブジェクトのリソースを引き継ぎつつ、新しいオブジェクトを初期化し、移動元を安全に破棄可能かつ代入可能な有効な状態に残します。適切なムーブ代入演算子は、既存のオブジェクトへリソースを転送し、自己代入の処理または許容を行い、代入先が保持していた既存リソースを解放または再利用した上で、移動元のリソースを引き継ぎ、移動元を安全な状態に残します。また、標準コンテナが要素の再配置時に例外安全保証を維持しながらムーブを利用できるよう、ムーブ操作には原則として noexcept を付与すべきです。

class Buffer {
public:
    Buffer() = default;
    explicit Buffer(std::size_t n) : size_(n), data_(new char[n]) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

private:
    std::size_t size_ = 0;
    char* data_ = nullptr;
};
AI コーチを使ってこの質問に答えてみる

4C++のAPIにおける const 正当性(const-correctness)と、const メンバ関数を効果的に設計する方法について説明してください。

const 正当性(const-correctness)とは、オブジェクトの観察可能(論理的)な状態を変更しない操作を型システムを通じて明示することを意味します。const メンバ関数は `this` オブジェクトが const 修飾されるため、非 mutable なデータメンバの変更や、同一オブジェクトに対する非 const メンバ関数の呼び出しができません。適切なAPI設計では、読み取り専用のクエリを const と指定し、必要に応じて値または const 参照/ポインタを返し、const 関数から変更可能な内部状態を外部に漏出させないようにします。`mutable` は、キャッシュ、遅延評価値、メトリクス、ミューテックスなど、論理的な状態を変更しない実装の詳細に限定して使用すべきです。const は変更に関するAPI上の規約であり、スレッドセーフ性を自動的に保証するものではありません。並行性に関する保証は別途実装およびドキュメント化が必要です。

#include <mutex>
#include <optional>
#include <string>

class UserProfile {
public:
    const std::string& id() const { return id_; }

    std::string displayName() const {
        std::lock_guard<std::mutex> lock(mu_);
        if (!cached_display_name_) {
            cached_display_name_ = computeDisplayName();
        }
        return *cached_display_name_;
    }

private:
    std::string computeDisplayName() const { return id_; }

    std::string id_;
    mutable std::mutex mu_;
    mutable std::optional<std::string> cached_display_name_;
};
AI コーチを使ってこの質問に答えてみる

5std::byte とは何ですか?また、C++において生バイナリバッファはどのように安全に表現すべきですか?

`std::byte` は、文字や算術演算用の整数ではなく、バイト単位の生バイナリデータを表現するために特化された個別の型です。ビット演算をサポートしつつ、バイトバッファが誤ってテキストや数値として扱われるのを防ぐため、型安全性が向上します。生バイナリバッファは、通常 `std::vector<std::byte>` や `std::array<std::byte, N>` のようなバイト指向のコンテナで表現し、所有権を持たないインターフェイスを経由して受け渡す場合は `std::span<std::byte>` または `std::span<const std::byte>` を使用すべきです。また、シリアライズ処理では任意のオブジェクトレイアウトに依存せず、値を明示的にエンコードおよびデコードする必要があります。

void write_frame(std::span<const std::byte> payload);

std::vector<std::byte> buf;
buf.push_back(std::byte{0x01});
buf.push_back(std::byte{0xFF});
write_frame(buf);
AI コーチを使ってこの質問に答えてみる

オブジェクトの寿命

6Rule of Zero、Rule of Three、Rule of Five の内容と、それぞれがどのような場合に適用されるかを説明してください。

Rule of Zero: カスタムのデストラクタ、コピー操作、ムーブ操作を宣言しないクラスを優先し、`std::string`、`std::vector`、`std::unique_ptr`、ファイル/ソケットのラッパーなどの RAII (Resource Acquisition Is Initialization) メンバーにリソース管理を委ねます。Rule of Three: クラスがリソースを手動で管理し、カスタムのデストラクタ、コピーコンストラクタ、またはコピー代入演算子を必要とする場合、適切なコピーと所有権の振る舞いを定義するために通常これら3つすべてを定義する必要があります。Rule of Five: C++11 以降では、そのような型はムーブコンストラクタとムーブ代入演算子の定義も検討すべきです。ほとんどのアプリケーションの型には Rule of Zero を適用します。型がリソースを直接所有している場合や、自明でない所有権や寿命のセマンティクスを持つ場合には Rule of Three または Rule of Five を適用します。

struct Session {
    std::string user_id;
    std::vector<std::string> roles;
    std::unique_ptr<Connection> conn;

    // No custom destructor/copy/move operations.
};
AI コーチを使ってこの質問に答えてみる

エラー処理

7C++における例外処理、スタック巻き戻し(stack unwinding)、デストラクタとの相互作用、およびサービスの例外境界について説明してください。

C++の例外は、`throw` 式から最も近い一致する `catch` 節へ制御を移行します。例外伝播の際、スタック巻き戻しによって完全に構築された自動変数のオブジェクトが逆順で破棄されるため、RAII (Resource Acquisition Is Initialization) によるリソース解放が自動的に実行されます。デストラクタは原則として例外を送出すべきではありません。`noexcept` なデストラクタから例外が脱出したり、巻き戻し処理の最中に別の例外が送出されたりすると、プログラムは `std::terminate` を呼び出します。オブジェクトのスライシングや不要なコピーを防ぐため、例外は通常参照(一般的には `const&`)でキャッチすべきです。バックエンドサービスでは、リクエストハンドラ、ワーカースレッドのエントリポイント、RPC/HTTPフレームワークのコールバック、`main` 関数などの例外境界を定義する必要があります。これらの境界で例外をログに記録し、エラーレスポンスやステータスコードに変換することで、C言語のAPI、デストラクタ、スレッド、`noexcept` 関数などの不適切なコンテキストへの例外の脱出を防ぎます。

struct Conn {
    ~Conn() noexcept { /* close or return connection to pool */ }
};

void handle() {
    Conn c;
    throw std::runtime_error("db failed");
} // c is destroyed while the exception propagates
AI コーチを使ってこの質問に答えてみる

8デストラクタが例外を送出した場合はどうなりますか。また、バックエンドの型はクリーンアップの失敗をどのように報告すべきですか。

通常、デストラクタは暗黙的に `noexcept(true)` となるため、そのようなデストラクタから例外が外部へ送出されると `std::terminate` が呼び出されます。デストラクタが明示的に `noexcept(false)` と宣言されている場合であっても、スタックアンワインド中に例外を送出することは危険です。別の例外がアクティブな状態で2つ目の例外が外部に送出されると、同様にプログラムが異常終了するためです。したがって、デストラクタはベストエフォートでクリーンアップを実行し、例外を外部に漏らさないようにすべきです。バックエンドの型は、クリーンアップの失敗をデストラクタの外で報告するために、破棄前にエラーまたは `std::expected` などを返すか例外を送出する `close()`、`flush()`、`commit()`、`stop()`、`shutdown()` などの明示的な操作を用意すべきです。デストラクタ内ではログ記録、メトリクス送信、エラーの抑制、または安全なフォールバックによるクリーンアップを実行できますが、対処が必要な失敗を報告するための主要な手段とすべきではありません。

class FileWriter {
public:
    std::expected<void, Error> close() noexcept; // reports flush/close failure explicitly

    ~FileWriter() noexcept {
        if (!closed_) {
            auto r = close();
            if (!r) log_error(r.error()); // destructor does not throw
        }
    }
private:
    bool closed_ = false;
};
AI コーチを使ってこの質問に答えてみる

標準ライブラリ

9`std::vector`はキャパシティ、拡張、再確保、およびイテレータの無効化(イテレータの安定性)をどのように管理しますか?

`std::vector`は要素を連続したメモリ領域に格納し、サイズ(`size`)とキャパシティ(`capacity`)の両方を追跡します。`size`は構築された要素数であり、`capacity`は新たなメモリ確保が必要になるまでに利用可能な割り当て済み要素ストレージの容量です。要素の追加によってキャパシティを超える場合、`vector`は通常、処理系定義の等比数列的な拡張戦略に従ってより大きなメモリ領域を確保し、既存の要素を移動またはコピーして、古い要素を破棄した上で古いストレージを解放します。`reserve(n)`は`size`を変更せずに`capacity`を増やしますが、`resize(n)`は要素を構築または破棄することで`size`を変更します。再確保が発生すると、既存の要素を指すイテレータ、参照、ポインタはすべて無効化されます。また、再確保が発生しなくても、`insert`や`erase`などの操作は変更位置またはそれ以降の位置にあるイテレータ等を無効化する可能性があります。

std::vector<int> v;
v.reserve(100);          // capacity >= 100, size == 0
v.push_back(1);          // size == 1
v.resize(10);            // size == 10, adds nine zero-initialized ints

std::cout << v.size() << " " << v.capacity() << "\n";
AI コーチを使ってこの質問に答えてみる

10連続メモリ配置型およびノードベースの標準コンテナについて、どのような無効化規則(イテレータや参照の無効化)を把握しておくべきですか?

無効化規則は、コンテナの種類および実行する操作によって異なります。 `vector` や `string` などの連続メモリ配置型コンテナは、イテレータや参照の安定性が脆弱です。要素の追加に伴うメモリの再確保(reallocation)が発生すると、すべてのイテレータ、参照、ポインタが無効化されます。また、再確保が発生しない場合でも、`insert` や `erase` は要素をシフトさせるため、変更箇所およびそれ以降の位置にあるイテレータや参照が無効化されます。 `list`、`map`、`set`、およびそれらの `multi` 系のようなノードベースの順序付きコンテナでは、一般に要素の挿入(`insert`)を行っても削除されない既存要素へのイテレータや参照は維持されます。要素を削除(`erase`)した場合は、その削除された要素へのイテレータと参照のみが無効化されます。 順序なしコンテナ(unordered containers)もノードに要素を格納するため、リハッシュ(rehash)が発生しても要素への参照やポインタは一般に維持されますが、イテレータは無効化されます。 `deque` は分割ストレージ構造を持つため、独自の規則が適用されます。実務では、コンテナの変更をまたいでイテレータや参照を保持する前に、対象のコンテナと操作に応じた具体的な規則を確認することが不可欠です。

std::map<int, std::string> m = {{1,"a"}, {2,"b"}, {3,"c"}};
for (auto it = m.begin(); it != m.end(); ) {
    if (it->first % 2 == 1)
        it = m.erase(it); // returns next iterator
    else
        ++it;
}
AI コーチを使ってこの質問に答えてみる

11バックエンドのルックアップテーブルにおいて、std::map、std::unordered_map、およびフラットマップ形式(flat-map-style)のコンテナを比較してください。

std::map は順序付きの(通常は木構造ベースの)連想コンテナであり、検索・挿入・削除を対数時間で行います。ソート順でのイテレーション、範囲クエリ、または順序保証が必要な場合に有用です。std::unordered_map はハッシュテーブルに基づいており、キーの順序は持ちませんが、完全一致のキー操作を平均して定数時間で行います。ハッシュ計算が適切であれば、大規模で変更の多いルックアップテーブルの適切なデフォルト選択肢となります。フラットマップ形式のコンテナはソート済みのキー/値のペアを連続したメモリ領域に格納するため、優れたキャッシュ局所性と、高速なイテレーションおよび二分探索による検索を提供しますが、中間への挿入や削除には線形時間がかかります。バックエンドのルックアップテーブルでは、ワークロードに順序や範囲検索が必要か、主に完全一致検索か、頻繁な変更が発生するか、予測可能なレイテンシが必要か、メモリオーバーヘッドやキャッシュの振る舞いなどを考慮して選択します。

// Exact lookup, frequently updated:
std::unordered_map<std::string, User> users_by_id;

// Need sorted iteration or lower_bound/range queries:
std::map<std::string, User> users_by_id_ordered;

// Build once, query many times: vector sorted by key is a common flat-map style.
std::vector<std::pair<std::string, User>> users;
std::sort(users.begin(), users.end(), [](auto const& a, auto const& b) {
    return a.first < b.first;
});

auto it = std::lower_bound(users.begin(), users.end(), std::string_view{"u123"},
    [](auto const& p, std::string_view key) { return p.first < key; });
AI コーチを使ってこの質問に答えてみる

12std::optionalと、値が存在しない状態を表すためのバックエンドにおける代表的なユースケースについて説明してください。

std::optional<T>は、保持されているT型の値、または「値が存在しない」状態のいずれかを表します。空の状態はstd::nulloptによって表され、コードからはhas_value()を呼び出すかboolコンテキストで評価して確認でき、*演算子やvalue()で値にアクセスし、value_or()でデフォルト値を指定できます。バックエンドコードでは、NULL許容のデータベースフィールド、リクエストや設定のオプションフィールド、データが存在しないことが想定されるキャッシュやリポジトリのミス、-1や空文字列のような番兵値では曖昧になるドメイン状態の表現などに有用です。これは値の欠落をモデル化するものであり、ポリモーフィズムや詳細なエラー情報を表現するためのものではありません。

struct UserProfile {
    std::string id;
    std::optional<std::string> display_name; // absent if user has not set it
};

std::string label(UserProfile const& u) {
    return u.display_name.value_or("anonymous");
}
AI コーチを使ってこの質問に答えてみる

13std::string_view と std::span とは何ですか。また、所有権を持たないビュー(non-owning view)はどのようなオブジェクトの寿命に関する危険性をもたらしますか。

std::string_view は連続した文字シーケンスの所有権を持たないビューであり、std::span<T> は型 T の連続したシーケンスの所有権を持たないビューです。これらはメモリの割り当てや所有を行わずにポインタと長さのみを保持するため、ゼロコピーの引数やバッファ操作インターフェイスに有用です。主な危険性はオブジェクトの寿命(lifetime)にあります。参照先のストレージはビューよりも長く生存しなければならず、ビューが使用されている間に無効化されてはなりません。一時オブジェクト、ローカル変数、破棄されたオブジェクト、またはメモリ再割り当てが行われたコンテナへのビューを戻り値として返したり保持したりすると、ダングリングビュー(dangling view)が発生する原因となります。

std::string_view bad() {
    std::string s = "hello";
    return std::string_view{s}; // dangling after return
}

void ok(std::string_view name) {
    // safe only during this call if caller's data outlives the call
}
AI コーチを使ってこの質問に答えてみる

並行処理

14std::mutex、std::shared_mutex、std::recursive_mutex はどのように異なり、それぞれどのような場合に選択すべきですか?

std::mutex は排他ロックを提供します。ロックを保持できるスレッドは1つだけであるため、共有の変更可能な状態を保護するためのデフォルトの選択肢となります。std::shared_mutex は共有リーダーロックと排他ライターロックをサポートします。複数のリーダーが同時にロックを保持できますが、ライターには排他的なアクセスが必要です。リーダーによる並行アクセスの恩恵がオーバーヘッドに見合い、かつライターの公平性や飢餓が許容または対処されている場合に、読み取り主体のデータに対して選択します。std::recursive_mutex は同一スレッドが複数回ロックを取得でき、同数回のアンロックが必要な排他ミューテックスです。不適切なロック設計を覆い隠してしまうリスクがあるため、主にレガシーコードや再入可能コード向けであり、使用する場面は極めて限定的です。

std::mutex m;
std::shared_mutex sm;
std::recursive_mutex rm;

// Default exclusive protection
{
    std::lock_guard<std::mutex> lk(m);
    // modify shared state
}

// Read-mostly protection
{
    std::shared_lock<std::shared_mutex> read_lk(sm); // many readers allowed
    // read shared state
}
{
    std::unique_lock<std::shared_mutex> write_lk(sm); // exclusive writer
    // modify shared state
}

// Recursive locking by same thread, usually avoid if possible
void legacy_api() {
    std::lock_guard<std::recursive_mutex> lk(rm);
    // may call another function that also locks rm
}
AI コーチを使ってこの質問に答えてみる

言語機能

15ラムダ式のキャプチャモードと、コールバックにおけるキャプチャとオブジェクトの寿命の相互作用について説明してください。

ラムダ式では、値キャプチャ(`[x]` や `[=]`)、参照キャプチャ(`[&x]` や `[&]`)、`this` のキャプチャ、および `[p = std::move(ptr)]` などの初期化キャプチャを使用できます。値キャプチャはラムダ式の生成時にキャプチャ対象のオブジェクトをクロージャにコピーします。一方、参照キャプチャは参照を保持するため、ラムダ式のすべての呼び出しが終わるまで元のオブジェクトが生存している必要があります。コールバック、保持されるラムダ式、または非同期処理においては、呼び出し前にローカル変数やオブジェクトが破棄される可能性があるため、参照キャプチャや `this` のキャプチャは危険です。必要なデータには値キャプチャを、所有権の移動にはムーブ/初期化キャプチャを使用し、オブジェクトの寿命を延長または確認する必要がある場合は意図的に `std::shared_ptr` や `std::weak_ptr` のパターンを使用することが推奨されます。

std::function<void()> make_bad_callback() {
    std::string name = "job";
    return [&] {
        // BUG: name is destroyed when make_bad_callback returns.
        std::cout << name << '\n';
    };
}

std::function<void()> make_good_callback() {
    std::string name = "job";
    return [name] {
        std::cout << name << '\n';
    };
}
AI コーチを使ってこの質問に答えてみる