C++ テンプレート実践技法と std::deque コンテナ解説

テンプレートクラスのインスタンス化と関数引数の受け渡し

汎用的なデータ構造を扱う際、クラステンプレートを活用することで型安全性と再利用性を高めることができます。その際に生成されたオブジェクトを関数へ引き渡す方法は、主に以下の3つに整理できます。

  1. 引数の明示的指定:呼び出す関数のシグネチャで具体的な型を記載する方式
  2. テンプレートパラメータの転送:関数側もテンプレートとし、呼び出し元の型情報をコンパイラに推論させる方式
  3. 完全なテンプレート化:関数の引数型自体を汎用型として扱い、内部的なアクセスプロトコルが共通していることを前提とする方式

実務では型安全を明確にするため、第1の方式が最もよく採用されますが、状況に応じて使い分けが可能です。以下に実装例を示します。

#include <iostream>
#include <string>
using namespace std;

// 汎用情報格納クラス
template <typename TType, typename TVal>
class DataPair {
public:
    DataPair(TType id, TVal value) : m_Id{id}, m_Value{value} {}
    void print() const {
        cout << "ID: " << m_Id << ", Value: " << m_Value << endl;
    }
private:
    TType m_Id;
    TVal m_Value;
};

// 方式1:特定の型を直接指定
void exportData1(DataPair<string, int>& obj) {
    obj.print();
}

// 方式2:関数をテンプレート化して型を伝播
template <typename A, typename B>
void exportData2(DataPair<A, B>& obj) {
    obj.print();
}

// 方式3:引数型をテンプレート化(ポリモーフィックな受け取り)
template <typename T>
void exportData3(T& obj) {
    obj.print();
}

int main() {
    DataPair<string, int> record{"S001", 42};

    exportData1(record); // string, int が固定
    exportData2(record); // コンパイラが自動推論
    exportData3(record); // T に DataPair<string, int> が束縛

    return 0;
}

テンプレートクラスと継承の整合性

ベースクラスがテンプレートである場合、派生クラスはメンバ変数のメモリ配置を確定させるために、基底クラスで使われるテンプレート引数の具体的な型を解決する必要があります。具体的には以下の2つのアプローチが存在します。

  • 派生クラスで基底の型を固定する:class Child : public Parent<double> のように明示的に型を指定する
  • 派生クラス自身もテンプレート化する:template<typename U> class Child : public Parent<U> とし、柔軟な型引き継ぎを実現する

型を硬性制約しない設計を目指す場合は後者の手段が適しています。

メンバー関数の外部定義

ヘッダーファイルでの宣言と、実装ファイルを別個に用意する場合、テンプレートメンバー関数はすべてテンプレート構文を含む必要があります。定義部には template<typename T1, typename T2> を前置きし、クラス名にも同じテンプレート引数を付与するのが規約です。

template <typename K, typename V>
class PairStore {
public:
    PairStore(K key, V val);
    void show() const;
private:
    K k_; V v_;
};

// 定義部の書き方
template <typename K, typename V>
PairStore<K, V>::PairStore(K key, V val) : k_{key}, v_{val} {}

template <typename K, typename V>
void PairStore<K, V>::show() const {
    cout << k_ << " | " << v_ << endl;
}

ヘッダーとソースへの分割における注意点

C++のテンプレートは、実際のパラメータが指定されて初めてコードが生成される(遅延インスタンシエーション)性質があります。このため、一般的なコンパイル単位ごとの分離を行うと、リンク時に未定義参照エラーが発生しやすい傾向があります。

これを回避する方法としては、実質的に一つのコンパイル単位として扱うために.cppファイルの末尾で#includeするか、あるいは宣言と定義を統合した.hpp形式での提供が一般的です。標準ライブラリに近いパッケージング手法となります。

フレンド関数との統合

非メンバ関数であっても、テンプレートクラスのプライベートメンバーにアクセスさせたい場合はフレンド宣言が必要です。クラススコープ内での即時定義と、外部からの参照指定では記述が異なります。

template <typename X, typename Y>
class Container;

// 外部実装のプレースホルド宣言
template <typename X, typename Y>
void inspect(Container<X, Y>& c);

template <typename X, typename Y>
class Container {
    // スコープ内即時定義
    friend void debugPrint(Container<X, Y>& c) {
        cout << c.getA() << "," << c.getB() << endl;
    }
    
    // スコープ外定義への公開
    friend void inspect<>(Container<X, Y>& c);
    
public:
    Container(X a, Y b) : a_{a}, b_{b} {}
    X getA() const { return a_; }
    Y getB() const { return b_; }
private:
    X a_; Y b_;
};

template <typename X, typename Y>
void inspect(Container<X, Y>& c) {
    cout << "External:" << c.a_ << "-" << c.b_ << endl;
}

std::deque コンテナの特性と操作

std::deque(ディーキュー)は、配列のようなランダムアクセス性と連結リストのような効率的な両端挿入・削除機能を備えたシーケンスコンテナです。内部実装は連続したメモリブロックのチャンクを管理しており、ベクトルとの主な差異は以下の通りです。

  • 先頭や末尾への要素追加・削除が O(1) で実行可能(vector では先頭操作が O(N) となるため大幅に高速)
  • 単一チャンク内の連続メモリの関係上、vector よりも少しアクセスオーバーヘッドが大きめ

主要なインターフェース一覧

コンストラクタ群

  • デフォルト構築: deque<T> d;
  • 範囲による初期化: deque<T> d(src_iter.begin(), src_iter.end());
  • 値による複製: deque<T> d(count, init_value);
  • コピー構築: deque<T> d(original_deque);

代入演算と割り当て

  • 代入演算子: d1 = d2;
  • イテレータ範囲の割り当て: d.assign(iter_start, iter_end);
  • 特定サイズでの埋め込み: d.assign(count, fill_value);

容量管理とサイズ取得

  • 空判定: d.empty();
  • 保持要素数の取得: d.size();
  • サイズの再設定: d.resize(new_size); または d.resize(new_size, pad_value);

リサイズ実行時、新たな容量が現在のサイズより大きい場合は末尾にデフォルト値または指定値が充填され、小さい場合は該当範囲を超える末尾の要素が切り捨てられます。メモリ効率とアクセス速度のバランスを取る必要があるデータストリーム処理において、強力な選択肢となり得ます。

タグ: C++ テンプレート ジェネリックプログラミング std::deque STL

5月13日 12:05 投稿