C++ 面接対策

C++ 面接問題

Backend Developer 向けに厳選した C++ の面接質問を、テーマ別にまとめたものです。EngineerSpeak の練習機能を支える同じ質問カタログから表示しています。

C++ Backend の 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スマートポインタにおけるカスタムデリータはどのように機能し、どのような場合にC/C++の相互運用性において有用ですか?

カスタムデリータとは、スマートポインタが所有するリソースを解放する際に、デフォルトの delete 処理の代わりに使用される呼び出し可能なクリーンアップ処理です。fclose、close、curl_easy_cleanup、SSL_free、free、各種ライブラリの破棄用関数など、特定の後処理関数で解放しなければならないリソースを扱う場合、C/C++相互運用の観点で有用です。unique_ptr ではデリータの型が unique_ptr 自体の型の一部となるためサイズに影響を与える可能性があり、ステートレスなデリータは最適化によってサイズゼロにできる一方、関数ポインタや状態を持つデリータはメモリを消費します。shared_ptr ではデリータは制御ブロック(control block)に格納され、最後の強い参照の所有者がリソースを解放した際に実行されます。カスタムデリータを活用することで、C言語のリソースも安全にRAII(Resource Acquisition Is Initialization)の管理下に置くことができます。

using FilePtr = std::unique_ptr<FILE, int(*)(FILE*)>;

FilePtr open_file(const char* path) {
    return FilePtr(std::fopen(path, "r"), &std::fclose);
}

struct CurlDeleter {
    void operator()(CURL* h) const noexcept {
        if (h) curl_easy_cleanup(h);
    }
};

using CurlPtr = std::unique_ptr<CURL, CurlDeleter>;
CurlPtr make_curl() {
    return CurlPtr(curl_easy_init());
}
AI コーチを使ってこの質問に答えてみる

4weak_ptr と shared_ptr の制御ブロック(control block)の仕組みについて、循環参照、enable_shared_from_this、および参照カウントのコストを含めて説明してください。

shared_ptr は、強参照カウントと弱参照カウント、およびデリータやアロケータなどの解放処理情報を含む制御ブロックを通じて共有所有権を管理します。shared_ptr オブジェクトのコピーや破棄を行うと、通常はアトミック操作によって強参照カウントが増減するため、異なるスレッド間で別々の shared_ptr オブジェクトを安全に操作できますが、参照カウントの更新ごとにコストが発生します。強参照カウントがゼロになると管理対象のオブジェクトは破棄されますが、制御ブロックは弱参照カウントもゼロになるまで保持されます。weak_ptr はオブジェクトの寿命を延ばすことなく同じ制御ブロックを参照します。lock() を呼び出すと、オブジェクトが生存していれば shared_ptr を返し、破棄されていれば空の shared_ptr を返します。shared_ptr 同士のみで循環参照を形成すると強参照カウントがゼロにならずメモリリークが発生するため、親へのポインタ(back-pointer)やオブザーバーの参照には weak_ptr が使用されます。enable_shared_from_this を使用すると、すでに shared_ptr によって所有されているオブジェクトが既存の制御ブロックを再利用して自身への新しい shared_ptr を作成できるため、二重に制御ブロックが生成される危険を防ぐことができます。参照カウントのコストとしては、アトミックな増減操作、キャッシュの競合、制御ブロック自体の割り当て、および性能上重要なコードパスにおけるオーバーヘッドが挙げられます。

struct Parent;

struct Child {
    std::weak_ptr<Parent> parent; // non-owning back-reference
};

struct Parent {
    std::vector<std::shared_ptr<Child>> children; // owns children
};

void example() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    c->parent = p;
    p->children.push_back(c);

    if (auto locked = c->parent.lock()) {
        // safe use while Parent is still alive
    }
}
AI コーチを使ってこの質問に答えてみる

型システム

5ムーブセマンティクス(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 コーチを使ってこの質問に答えてみる

6C++の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 コーチを使ってこの質問に答えてみる

7ムーブ専用型とは何ですか?また、ソケット、ファイルハンドル、ロックなどのAPI境界にどのような影響を与えますか?

ムーブ専用型とは、コピーはできないもののムーブは可能な型のことです。ソケット、ファイル記述子(ファイルディスクリプタ)、ファイルハンドル、ロック、`std::unique_ptr` など、リソースの単独所有権を管理するためによく用いられます。二重所有や二重解放を防ぐためにコピー操作は削除(delete)され、ムーブ操作によって所有権が移動し、ムーブ元のオブジェクトは有効ではあるものの通常は空(非所有)の状態になります。APIでは所有権の境界を明示的にすべきです。ファクトリ関数はムーブ専用オブジェクトを値渡しで返すことができ、所有権を受け取る関数は値渡しまたは右辺値参照で受け取ることができます。また、参照のみを行う関数は参照、ポインタ、またはその他の借用ハンドルを受け取るべきです。コンテナは、要素がムーブによって挿入または再配置される場合にムーブ専用の値を格納できます。

#include <utility>

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

    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) {
            close_if_valid();
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }
    explicit operator bool() const noexcept { return fd_ != -1; }

private:
    void close_if_valid() noexcept {
        if (fd_ != -1) {
            // ::close(fd_); // real POSIX code would close here
            fd_ = -1;
        }
    }

    int fd_;
};

Fd open_socket();               // factory returns ownership by value
void take_socket(Fd fd);        // takes ownership
void inspect_socket(const Fd&); // borrows only

void example() {
    Fd s = open_socket();
    inspect_socket(s);
    take_socket(std::move(s));
}
AI コーチを使ってこの質問に答えてみる

8一般的なバックエンドコードにおけるauto、decltype、decltype(auto)、およびテンプレート引数推論の違いを比較してください。

autoは変数に対してテンプレートに類似した型推論を使用します。通常のautoは、宣言時にauto&、const auto&、auto&&のように明示的に指定しない限り、通常は参照や最上位のconstを脱落(drop)させます。decltype(expr)は宣言された型や式の型をより厳密に取得します。括弧なしの識別子式は宣言された型を返し、それ以外のlvalue式はT&、xvalueはT&&、prvalueはTを生成します。decltype(auto)はdecltypeの規則に沿って推論し、参照を保持する必要がある戻り値の型などでよく利用されます。テンプレート引数推論はautoと類似していますが、T、T&、const T&、T&&などの仮引数の形式に依存し、独自の規則を持ちます。波括弧による初期化子はよく見られる違いの一つであり、auto x = {1, 2}はstd::initializer_list<int>と推論されますが、通常のテンプレート仮引数では、仮引数がinitializer_listやその他の適切な型を期待していない限り、単独の波括弧初期化子からTを推論することは一般にできません。

int x = 1;
const int cx = 2;
int& rx = x;

auto a = rx;        // int, copy of x
auto b = cx;        // int, top-level const dropped
const auto& c = cx; // const int&, reference preserved by declaration

a = 10;             // does not change x
AI コーチを使ってこの質問に答えてみる

オブジェクトの寿命

9Rule 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 コーチを使ってこの質問に答えてみる

言語セマンティクス

10C++の初期化形式と、initializer_listや集約体(aggregates)を含むよくある落とし穴について説明してください。

C++には複数の初期化形式があります。`T x;` などのデフォルト初期化(default initialization)は、クラス型の場合はデフォルトコンストラクタを呼び出しますが、自動変数の基本型は未初期化のままになります。`T x{};` や `T()` などの値初期化(value initialization)は、コンストラクタやメンバの初期化前に適用可能な箇所をゼロ初期化します。中括弧を用いるリスト初期化(list initialization)は縮小変換(narrowing conversion)を禁止し、有効な `std::initializer_list` コンストラクタが強く優先されるなどの特別なオーバーロード解決ルールを持ちます。集約体初期化(aggregate initialization)は、中括弧を使って集約体のメンバを直接初期化します。C++20からは宣言順に従う指示付き初期化子(designated initializers)もサポートされています。よくある落とし穴としては、ローカルのスカラー型の未初期化、意図しない `initializer_list` オーバーロードの選択、中括弧での縮小変換エラー、丸括弧による最も厄介な構文解析(most vexing parse)、型が集約体でなくなった際の動作変化などが挙げられます。

#include <iostream>

struct S {
    int x;
};

int main() {
    int a;      // default-initialized: indeterminate value
    int b{};    // value/list-initialized: zero
    S s{};      // aggregate/value initialization: s.x is zero

    std::cout << b << ' ' << s.x << '\n';
    (void)a;    // reading a would be undefined behavior
}
AI コーチを使ってこの質問に答えてみる

11C++における未定義動作(UB: Undefined Behavior)とは何ですか。また、バックエンドの本番環境のインシデントではどのように顕在化しますか?

未定義動作(UB: Undefined Behavior)とは、無効な操作が発生した後にC++標準仕様が一切の動作要件を課さない状態のことです。プログラムは正常に動作しているように見えることもあれば、クラッシュ、データの破損、セキュリティ上の脆弱性の発生、または最適化によって予期しない振る舞いに変化することもあります。コンパイラはUBが発生しないことを前提として最適化を行うため、リリースビルドや本番環境のトラフィック下でのみ問題が顕在化することがあります。バックエンドのインシデントの原因としては、ダングリングポインタ/参照、解放後使用(use-after-free)、オブジェクトの寿命違反、範囲外アクセス、符号付き整数のオーバーフロー、データ競合、無効なキャスト、未初期化変数の読み取り、二重解放、厳密なエイリアシングルール(strict-aliasing)の違反などが挙げられます。対策としては、RAII(Resource Acquisition Is Initialization)や明確な所有権・寿命設計、安全な抽象化と境界チェック、テストやファジング、コードレビュー、静的解析、そしてASan、UBSan、TSanなどのサニタイザの活用があります。

int grow(int x) {
    if (x + 1 < x) {
        return 0; // compiler may assume this is unreachable
    }
    return x + 1;
}
AI コーチを使ってこの質問に答えてみる

Object Model

12仮想関数、仮想関数テーブル(vtable)、動的ディスパッチのコスト、オブジェクトのスライシング、および仮想デストラクタに関する危険性について説明してください。

仮想関数は実行時ポリモーフィズムを実現します。基底クラスのポインタまたは参照を介して呼び出された際、オブジェクトの動的型に応じた実装が選択されます。多くの実装では、各ポリモーフィックなオブジェクト内に隠された vptr を保持し、その動的型に対応する仮想関数アドレスの仮想関数テーブル(vtable)を指します。一般的なコストとしては、オブジェクトごとのポインタの追加、間接呼び出し、キャッシュや分岐予測への影響の可能性、インライン化機会の減少などが挙げられますが、コンパイラが非仮想化(devirtualize)できる場合もあります。オブジェクトのスライシングは、派生クラスのオブジェクトが基底クラスのオブジェクトとして値渡しまたは値でコピーされた際に発生し、派生部分と動的な振る舞いが失われます。基底クラスのポインタを介してオブジェクトを破棄することが想定されている場合、そのデストラクタは仮想デストラクタでなければなりません。そうでなければ、その基底ポインタを介して派生オブジェクトを削除する処理は未定義動作となります。

#include <iostream>

struct Handler {
    virtual void handle() { std::cout << "base\n"; }
    virtual ~Handler() = default;
};

struct HttpHandler : Handler {
    void handle() override { std::cout << "http\n"; }
};

void run(Handler& h) {
    h.handle(); // virtual dispatch
}

int main() {
    HttpHandler h;
    run(h);
}
AI コーチを使ってこの質問に答えてみる

標準ライブラリ

13サービスのタイムアウト、メトリクス、およびタイムスタンプにおける`std::chrono`のクロック(clock)、タイムポイント(time point)、期間(duration)について説明してください。

`std::chrono`は、クロック(clock)、`time_point`、`duration`を用いて時間を表現します。`duration`はミリ秒や秒などの単位を持つ時間間隔です。`time_point`は特定のクロックの時間軸上における一時点を表します。`steady_clock`は単調増加(モノトニック)であり、実時間(壁時計時間)の変更に影響されないため、経過時間、サービスのタイムアウト、デッドライン、レイテンシ測定に適しています。`system_clock`は市民時/実時間を表し、タイムスタンプ、ロギング、永続化、カレンダー変換に適していますが、システム時刻の同期や調整によって値が前後する可能性があります。タイムアウトは通常`steady_clock::now() + duration`に基づいて実装すべきであり、機械処理用のタイムスタンプには明確に定義された実時間形式(通常、APIやストレージの境界ではUTC(Coordinated Universal Time / 協定世界時))を使用すべきです。

using namespace std::chrono_literals;

auto deadline = std::chrono::steady_clock::now() + 500ms;
while (!done()) {
    if (std::chrono::steady_clock::now() >= deadline) {
        throw std::runtime_error("timeout");
    }
    poll_once();
}
AI コーチを使ってこの質問に答えてみる

14iostreams や printf スタイルの API と比較して、std::format およびモダンなフォーマット機能について説明してください。

`std::format` は fmtlib に着想を得た C++20 の型安全なフォーマット機能です。iostreams のような状態を持つ挿入構文や、`printf` スタイルの C 言語可変長引数を使わずに、`{}` による置換フィールドとフォーマット指定子を用いてフォーマットされた文字列を生成します。`printf` と比較すると、フォーマット指定子と型の不一致による多くの問題を回避でき、iostreams と比較すると、より簡潔で組み立てが容易です。fmtlib は `std::format` に先行し影響を与えた広く使われているライブラリであり、より広範なサポートや新しい機能を提供する場合があります。ユーザー定義型はカスタムフォーマッタをサポートすることでフォーマット可能です。大量のログ出力においては、特に無効化されているログレベルにおいて不要なフォーマット処理、型変換、メモリ割り当てを回避できるかどうかに性能が左右されます。そのため、フォーマット処理を遅延させるか、事前にログレベルを確認する API が推奨されます。

#include <format>
#include <string>

std::string msg = std::format("user={} latency_ms={:.2f}", user_id, latency_ms);
AI コーチを使ってこの質問に答えてみる

テンプレート

15C++ の値カテゴリ(value categories)と完全転送(perfect forwarding)について説明し、これらが効率的なジェネリック API の設計においてなぜ重要なのかを述べてください。

C++ の値カテゴリは式(expression)の性質を分類したものです。lvalue(左辺値)は識別性を持ち、式が評価された後も参照可能です。prvalue(純粋右辺値)は多くの一時オブジェクトや計算結果のような純粋な右辺値であり、xvalue(消滅値)はリソースの再利用が可能な有効期限切れ間近のオブジェクトを表します。完全転送とは、型推論を伴う T&& などの転送参照(forwarding reference)を受け取り、std::forward<T>(arg) を用いて転送することで、呼び出し元の値カテゴリ(lvalue は lvalue のまま、rvalue は rvalue のまま)を維持するテンプレート技術です。これは参照の縮約(reference collapsing)規則によって実現されています。ラッパー、ファクトリ、ディスパッチャ、emplace 系の関数において不要なコピーを回避し、オーバーロード解決やムーブの挙動を正しく保つことができるため、効率的なジェネリックバックエンド API において不可欠です。

#include <iostream>
#include <string>
#include <utility>

void sink(const std::string&) { std::cout << "const ref\n"; }
void sink(std::string&&) { std::cout << "rvalue ref\n"; }

template <class T>
void wrapper(T&& arg) {
    sink(std::forward<T>(arg));
}

int main() {
    std::string s = "hello";
    wrapper(s);
    wrapper(std::string{"x"});
}
AI コーチを使ってこの質問に答えてみる

並行処理

16std::scoped_lock や std::lock を使用したデッドロック防止策および複数ミューテックスのロック戦略について説明してください。

デッドロックは循環待機を排除することで防止できます。可能な場合はシステム全体で一貫した順序でロックを取得するか、デッドロック回避アルゴリズムを用いる `std::lock` または `std::scoped_lock` を使用して複数のミューテックスを取得します。`std::scoped_lock lock(a, b, ...)` は、複数のミューテックスをロックし、スコープを抜ける際に自動でアンロックするための最もシンプルな RAII (Resource Acquisition Is Initialization) 形式です。`std::lock` の場合は、先にミューテックス群をロックしてから `std::adopt_lock` を使ってRAIIラッパーに関連付けるか、`std::defer_lock` を指定した `std::unique_lock` と組み合わせて使用します。クリティカルセクションは短く保ち、ロックを保持したままブロッキング処理や出所不明のコールバックを実行しないようにしてください。なお、デッドロック回避は公平性を自動的に保証するものではなく、ライブロックやスタベーションのすべてのシナリオを防げるわけではありません。

struct Account {
    std::mutex m;
    int balance{};
};

void transfer(Account& from, Account& to, int amount) {
    if (&from == &to) return;

    std::scoped_lock lock(from.m, to.m);

    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance += amount;
    }
}
AI コーチを使ってこの質問に答えてみる

17状態変化を待機する際の condition_variable の使用パターンについて説明してください。

ミューテックスで保護された状態の述語(predicate)の変化を待機するには `std::condition_variable` を使用します。共有状態は同じミューテックスを保持した状態で変更され、その後 `notify_one` または `notify_all` を呼び出して待機中のスレッドを起床させます。スプリアス起床(偽の目覚め)が発生する可能性があり、また通知自体は述語の状態とは独立して保持されないため、待機側は `cv.wait(lock, predicate)` または同等のループ処理を使用する必要があります。`notify_one` は待機中のスレッドを1つだけ起床させ、`notify_all` はすべての待機スレッドを起床させるため、シャットダウンなどのブロードキャストによる状態変更に適しています。

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool closed = false;

void push(int x) {
    {
        std::lock_guard<std::mutex> lk(m);
        q.push(x);
    }
    cv.notify_one();
}

std::optional<int> pop() {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [&] { return !q.empty() || closed; });

    if (q.empty()) return std::nullopt; // closed and drained
    int x = q.front();
    q.pop();
    return x;
}

void close() {
    {
        std::lock_guard<std::mutex> lk(m);
        closed = true;
    }
    cv.notify_all();
}
AI コーチを使ってこの質問に答えてみる

18標準C++のプリミティブを使用してスレッドセーフな有界キュー(bounded queue)を設計し、そのAPI保証を定義してください。

有界スレッドセーフキューは、`std::mutex`、条件変数、固定容量のコンテナ、およびシャットダウン/クローズフラグを用いて実装できます。`push` はキューが満杯のときにブロック、タイムアウト、または失敗するようにしてバックプレッシャーを提供し、`pop` は空のときにブロックします。どちらの操作も、`size < capacity || closed` や `!empty || closed` などの述語を待機条件とする必要があります。シャットダウン/クローズ時には、ブロックされているプロデューサーとコンシューマーを起こし、新しい `push` を拒否した上で、コンシューマーが既存の要素を最後まで取り出す(drain)か即座に停止するかを明確に定義します。APIではブロッキング動作、クローズセマンティクス、戻り値、および並行性の保証を規定する必要があります。

template<class T>
class BoundedQueue {
public:
    explicit BoundedQueue(std::size_t cap) : cap_(cap) {}

    // Returns false if the queue has been closed before the item is accepted.
    bool push(T value) {
        std::unique_lock<std::mutex> lk(m_);
        not_full_.wait(lk, [&] { return q_.size() < cap_ || closed_; });
        if (closed_) return false;

        q_.push_back(std::move(value));
        lk.unlock();
        not_empty_.notify_one();
        return true;
    }

    // Returns nullopt when closed and drained.
    std::optional<T> pop() {
        std::unique_lock<std::mutex> lk(m_);
        not_empty_.wait(lk, [&] { return !q_.empty() || closed_; });
        if (q_.empty()) return std::nullopt;

        T value = std::move(q_.front());
        q_.pop_front();
        lk.unlock();
        not_full_.notify_one();
        return value;
    }

    void close() {
        {
            std::lock_guard<std::mutex> lk(m_);
            closed_ = true;
        }
        not_empty_.notify_all();
        not_full_.notify_all();
    }

private:
    const std::size_t cap_;
    std::mutex m_;
    std::condition_variable not_empty_;
    std::condition_variable not_full_;
    std::deque<T> q_;
    bool closed_ = false;
};
AI コーチを使ってこの質問に答えてみる

Database

19ダウンタイムなしでC++サービスのスキーマ移行を実行するにはどうすればよいですか?

段階的な Expand-Contract(展開・縮小)移行パターンを使用します。まず、稼働中のC++サービスを破壊しない、NULL許容カラムの追加や新規テーブルの作成など、下位互換性のあるスキーマ変更を行います。次に、新旧両方のデータ表現を処理できる互換性のあるコードをデプロイし、既存データを負荷を抑えた小さなバッチでバックフィルし、整合性を検証した上で、読み取りを新しいスキーマに切り替えます。デプロイ済みのすべてのバージョンが古いスキーマに依存しなくなった後に、古いカラムやコードを削除します。大規模なテーブルでは、長時間ロックするブロッキングなDDL(Data Definition Language)操作を避け、対応している環境ではオンライン/並行(concurrent)操作を使用し、ロック、レプリケーション、リソースを監視しつつ、部分的にデプロイされたコードや部分的に移行されたデータでも動作するロールバック手順を維持します。

1. Expand: add new nullable column user_display_name.
2. Deploy compatible code: write both name and user_display_name; read new if present, else old.
3. Backfill: copy name -> user_display_name in small batches.
4. Validate: compare counts/checksums/null rates and monitor errors.
5. Switch: read only user_display_name after confidence.
6. Contract: stop writing old name, then drop old column in a later release.
AI コーチを使ってこの質問に答えてみる

20C++サービスにおいて、古いデータの読み取り(stale read)の制御や自分の書き込みの即時読み取り(read-your-writes)保証を管理しながら、データベースレプリカから読み取りを行うにはどうすべきですか?

レプリカからの読み取りは、データ整合性に関する明示的なトレードオフとして扱います。ある程度の遅延(staleness)を許容できる読み取りは正常なレプリカにルーティングできますが、read-your-writes(自身の書き込みの読み取り保証)やトランザクション処理、データの鮮度が不可欠な読み取りはプライマリにルーティングする必要があります。ただし、選択したレプリカが対象の書き込み位置(LSN: Log Sequence Number、タイムスタンプ、バージョンなど)まで確実に追従(リプレイ)していることが確認できる場合はレプリカからの読み取りも可能です。レプリカの遅延状況とヘルス状態を監視し、エンドポイントまたはリクエスト単位で要求される整合性レベルを指定します。レプリカの遅延が許容範囲を超えた場合は、プライマリへのフォールバック、追従待ち、またはリクエストの失敗などの制御を行います。呼び出し元がどの読み取りで古いデータが返る可能性があるかを把握できるよう、整合性モデルを文書化しておくことも重要です。

ReadPolicy EVENTUAL(max_staleness=2s):
  choose healthy replica with lag <= 2s; else primary or error depending on SLA

ReadPolicy READ_YOUR_WRITES(session_lsn):
  choose replica only if replay_lsn >= session_lsn;
  otherwise wait briefly or route to primary

ReadPolicy STRONG:
  route to primary
AI コーチを使ってこの質問に答えてみる