コンテンツにスキップ

Modern More C++ Idioms

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

この文書では、C++17から始まり、C++20、そしてC++23にかけての言語と標準ライブラリの革新的な変更を踏まえて、従来の「More C++ Idioms」を現代的にアップデートします。プログラマに役立つパターンを具体的なコード例と共に解説し、古いイディオムを新しい言語機能でどう書き換えるかを明示します。

基本イディオム

[編集]

改良: RAII (Resource Acquisition Is Initialization)

[編集]

RAII は C++ の最も強力なイディオムの一つですが、C++11以降のスマートポインタと、C++23で導入されたstd::expectedにより、より柔軟で表現力豊かな実装が可能になりました。

従来のパターン

[編集]

以下に、古典的なRAIIパターンの例を示します。ファイルハンドルを安全に管理する例です:

class File {
  FILE* f;
public:
  File(const char* filename) : f(fopen(filename, "r")) {
    if (!f) throw std::runtime_error("Could not open file");
  }
  ~File() { if (f) fclose(f); }
  // コピー禁止
  File(const File&) = delete;
  File& operator=(const File&) = delete;
};
このアプローチには以下の問題があります:
  • 明示的なリソース解放コードが必要
  • ムーブセマンティクスの実装が必要
  • エラー処理が例外に依存

モダンなアプローチ(C++17)

[編集]

C++17では、std::unique_ptrとカスタムデリータを組み合わせることで、よりエレガントな実装が可能です:

class File {
  std::unique_ptr<FILE, decltype(&fclose)> f;
public:
  File(const char* filename) 
    : f(fopen(filename, "r"), &fclose) {
    if (!f) throw std::runtime_error("Could not open file");
  }
  
  // ファイルの読み取り操作
  std::optional<std::string> read_line() {
    if (!f) return std::nullopt;
    
    char buffer[1024];
    if (fgets(buffer, sizeof(buffer), f.get())) {
      return std::string(buffer);
    }
    return std::nullopt;
  }
};
改善点
  • デストラクタで特別な処理が不要
  • ムーブ操作が自動的にサポートされる
  • リソース管理とビジネスロジックの分離

C++23 による革新的アプローチ: std::expected

[編集]

C++23で導入されたstd::expectedを使うと、例外を使わないエラー処理と組み合わせた現代的なRAIIが実現できます:

struct FileError {
  int error_code;
  std::string message;
};

class File {
private:
  std::unique_ptr<FILE, decltype(&fclose)> f;
  
public:
  explicit File(FILE* file_handle) 
    : f(file_handle, &fclose) {}
  
  // ファイル操作メソッド...
};

std::expected<File, FileError> open_file(const char* filename) {
  FILE* raw_file = fopen(filename, "r");
  if (!raw_file) {
    return std::unexpected(FileError{
      errno, 
      std::format("ファイルオープンエラー: {}", strerror(errno))
    });
  }
  return File{raw_file};
}

// 使用例
void process_file(const char* filename) {
  auto file_result = open_file(filename);
  
  if (!file_result) {
    std::cerr << "エラー: " << file_result.error().message << '\n';
    return;
  }
  
  File& file = file_result.value();
  // ファイル処理...
} // ファイルは自動的にクローズ
このアプローチの利点
  • 例外を使わないエラー処理
  • エラー情報の詳細な伝播
  • コンパイル時の型安全性
  • リソース管理の自動化

改良: PIMPL (Pointer to Implementation)

[編集]

PIMPLイディオムは、ヘッダーファイルからの実装詳細の隠蔽とABIの安定性を確保するための重要なパターンです。C++20のモジュールとコンセプトにより、より柔軟で型安全な実装が可能になりました。

従来のパターン

[編集]
// header.h
class Widget {
private:
  class Impl;
  std::unique_ptr<Impl> pImpl;
public:
  Widget();
  ~Widget();
  void doSomething();
};

// source.cpp
class Widget::Impl {
public:
  void doSomething() { 
    std::cout << "Doing something...\n"; 
  }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 必要な前方宣言
void Widget::doSomething() { pImpl->doSomething(); }
注意点
  • デストラクタを必ず.cppファイルで定義する必要がある
  • 不完全型に対するunique_ptrの動作に依存
  • コンパイラの最適化を制限する

C++20 モジュールとコンセプトによる改良

[編集]
// widget.cppm
module;
#include <memory>
#include <functional>

export module widget;

export class Widget {
public:
  // コンセプトによる制約付きテンプレートコンストラクタ
  template<typename T>
  requires std::invocable<T>
  explicit Widget(T&& custom_behavior);
  
  Widget();
  ~Widget();
  void doSomething();

private:
  class Impl;
  std::unique_ptr<Impl> pImpl;
};

// widget_impl.cpp
module widget;
#include <utility>
#include <iostream>

class Widget::Impl {
  std::function<void()> custom_action;
public:
  Impl() : custom_action([](){
    std::cout << "Default implementation\n";
  }) {}
  
  template<typename T>
  Impl(T&& action) : custom_action(std::forward<T>(action)) {}
  
  void doSomething() { 
    std::cout << "Widget implementation: ";
    custom_action(); 
  }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

template<typename T>
requires std::invocable<T>
Widget::Widget(T&& custom_behavior)
  : pImpl(std::make_unique<Impl>(std::forward<T>(custom_behavior))) {}

Widget::~Widget() = default;

void Widget::doSomething() { pImpl->doSomething(); }
モジュールとコンセプトによる改善点
  • モジュールによるインターフェースと実装の明確な分離
  • コンセプトによる型制約で安全性が向上
  • 柔軟な振る舞いのカスタマイズ
  • ビルド時間の短縮(モジュールのプリコンパイルによる)
使用例
import widget;
#include <iostream>

auto main() -> int {
  // デフォルト動作
  Widget w1;
  w1.doSomething();  // "Widget implementation: Default implementation"
  
  // カスタム動作
  Widget w2([]{ std::cout << "Custom action!\n"; });
  w2.doSomething();  // "Widget implementation: Custom action!"
}

新規: 構造化束縛とタプル操作

[編集]

C++17で導入された構造化束縛は、複数の戻り値をエレガントに扱う方法を提供し、C++23ではさらに強化されました。

データアクセスの簡素化

[編集]
従来のアプローチ
std::tuple<int, std::string, double> getData() {
  return {42, "answer", 3.14};
}

void processData() {
  auto data = getData();
  int id = std::get<0>(data);
  std::string name = std::get<1>(data);
  double value = std::get<2>(data);
  
  // データ処理...
}
C++17の構造化束縛
void processData() {
  auto [id, name, value] = getData();
  
  // より読みやすいコード
  std::cout << "ID: " << id << ", Name: " << name 
            << ", Value: " << value << '\n';
}

C++23のタプル操作強化

[編集]

C++23では、タプルの操作がさらに強化され、特にstd::tuple_elementと新しいタプル操作関数によって柔軟性が向上しました。

#include <tuple>
#include <format>
#include <iostream>

// タプルに対する汎用的な操作
template<typename Tuple>
void printTupleInfo(const Tuple& t) {
  constexpr size_t size = std::tuple_size_v<Tuple>;
  
  std::cout << "Tuple contains " << size << " elements:\n";
  
  // C++23のfold expressions拡張と組み合わせた例
  [&]<size_t... I>(std::index_sequence<I...>) {
    ((std::cout << std::format("  Element {}: {}\n", I, std::get<I>(t))), ...);
  }(std::make_index_sequence<size>{});
}

// 構造化束縛とタプルの連携
struct UserData {
  int id;
  std::string name;
  double score;
  
  // C++23: デフォルトでタプルライクな動作
  auto operator<=>(const UserData&) const = default;
};

// C++23: 構造化束縛とconstexpr if
template<typename T>
void processAny(const T& data) {
  if constexpr (requires { 
    std::tuple_size<T>::value; 
    std::get<0>(data); 
  }) {
    // タプルライクな型
    printTupleInfo(data);
  } else if constexpr (requires { data.id; data.name; }) {
    // UserDataライクな型
    auto [id, name, score] = data;
    std::cout << std::format("User: {} (ID: {}, Score: {})\n", 
                             name, id, score);
  } else {
    std::cout << "Unknown data type\n";
  }
}
使用例
auto main() -> int {
  auto tuple_data = std::make_tuple(1, "Example", 9.5);
  UserData user_data{42, "Alice", 95.5};
  
  processAny(tuple_data);
  processAny(user_data);
}
このアプローチの利点
  • より宣言的で読みやすいコード
  • データ構造へのアクセスの統一
  • ジェネリックプログラミングとの親和性
  • コンパイル時の型安全性

メモリ管理イディオム

[編集]

改良: Copy-and-Swap

[編集]

Copy-and-Swapイディオムは安全なリソース管理のための伝統的なパターンですが、C++23の新しい機能でさらに強化できます。

従来のパターン

[編集]
class Resource {
  int* data;
  size_t size;

public:
  // コンストラクタ
  Resource(size_t sz) : size(sz), data(new int[sz]) {}
  
  // コピーコンストラクタ
  Resource(const Resource& other) : size(other.size), data(new int[other.size]) {
    std::copy(other.data, other.data + size, data);
  }
  
  // コピー代入演算子
  Resource& operator=(const Resource& other) {
    Resource temp(other);  // コピーコンストラクタを使用
    swap(temp);           // 例外安全なスワップ
    return *this;
  }
  
  // スワップ関数
  void swap(Resource& other) noexcept {
    std::swap(data, other.data);
    std::swap(size, other.size);
  }
  
  // デストラクタ
  ~Resource() { delete[] data; }
};

C++23による改良: noexceptの最適化とムーブセマンティクス

[編集]
class Resource {
  std::unique_ptr<int[]> data;
  size_t size;

public:
  // コンストラクタ - noexcept条件付き
  Resource(size_t sz) noexcept(noexcept(std::make_unique<int[]>(sz)))
    : size(sz), data(std::make_unique<int[]>(sz)) {}
  
  // コピーコンストラクタ
  Resource(const Resource& other)
    : size(other.size), data(std::make_unique<int[]>(other.size)) {
    std::copy(other.data.get(), other.data.get() + size, data.get());
  }
  
  // ムーブコンストラクタ - 常にnoexcept
  Resource(Resource&& other) noexcept
    : size(other.size), data(std::move(other.data)) {
    other.size = 0;
  }
  
  // 統合コピー/ムーブ代入演算子
  Resource& operator=(Resource other) noexcept {
    swap(other);  // other はスタック上のコピーか移動済みオブジェクト
    return *this;
  }
  
  // スワップ関数 - フリー関数と友達関係
  friend void swap(Resource& first, Resource& second) noexcept {
    using std::swap;
    swap(first.data, second.data);
    swap(first.size, second.size);
  }
};

// std::swapの特殊化
namespace std {
  template<>
  void swap<Resource>(Resource& a, Resource& b) noexcept {
    ::swap(a, b);  // 友達関数を呼び出し
  }
}

C++23での進化: move_only_functionとの連携

C++23で導入されたstd::move_only_functionは、ムーブのみ可能な関数オブジェクトを提供します。これをCopy-and-Swapパターンと組み合わせると、リソースを所有する関数オブジェクトを効率的に管理できます:

#include <functional>
#include <memory>
#include <utility>

class CallbackManager {
  std::vector<std::move_only_function<void()>> callbacks;

public:
  // コールバック追加
  void add_callback(std::move_only_function<void()> cb) {
    callbacks.push_back(std::move(cb));
  }
  
  // Copy-and-Swapパターンによる代入演算子
  CallbackManager& operator=(CallbackManager other) noexcept {
    swap(*this, other);
    return *this;
  }
  
  // ムーブコンストラクタ
  CallbackManager(CallbackManager&& other) noexcept 
    : callbacks(std::move(other.callbacks)) {}
  
  // コピーコンストラクタは禁止(move_only_functionがコピーできないため)
  CallbackManager(const CallbackManager&) = delete;
  
  // デフォルトコンストラクタ
  CallbackManager() = default;
  
  // フレンドスワップ関数
  friend void swap(CallbackManager& first, CallbackManager& second) noexcept {
    using std::swap;
    swap(first.callbacks, second.callbacks);
  }
  
  // コールバック実行
  void execute_all() {
    for (auto& cb : callbacks) {
      cb();
    }
  }
};
改良点
  • noexcept指定による最適化の機会
  • std::unique_ptrによるリソース管理の簡素化
  • ムーブセマンティクスと統合された代入演算子
  • C++23のmove_only_functionとの連携

改良: Counted Body/Reference Counting

[編集]

参照カウントは共有リソース管理の基本テクニックですが、C++20とC++23では、std::shared_ptrのパフォーマンスとアトミック操作が強化されました。

従来のパターン

[編集]
// 手動実装の参照カウント
class CountedBody {
  int ref_count = 0;
public:
  void add_ref() { ++ref_count; }
  void release() { if (--ref_count == 0) delete this; }
};

class Widget {
  CountedBody* body;
public:
  Widget(CountedBody* b) : body(b) { body->add_ref(); }
  Widget(const Widget& other) : body(other.body) { body->add_ref(); }
  ~Widget() { body->release(); }
};

C++20/23: std::shared_ptrの最適化テクニック

[編集]
#include <memory>
#include <atomic>

// 効率的なshared_ptrの使用
class Widget {
public:
  // make_sharedによる割り当て最適化
  static Widget create() {
    return Widget(std::make_shared<Impl>());
  }
  
  // カスタムデリータとアロケータ
  static Widget create_custom() {
    auto deleter = [Impl* p ] { 
      std::cout << "Custom deletion\n";
      delete p;
    };
    
    return Widget(std::shared_ptr<Impl>(new Impl(), deleter));
  }
  
  // アロケータ対応バージョン
  template<typename Alloc>
  static Widget create_with_allocator(const Alloc& alloc) {
    return Widget(std::allocate_shared<Impl>(alloc));
  }
  
private:
  class Impl {
    // 実装詳細
  };
  
  std::shared_ptr<Impl> impl;
  explicit Widget(std::shared_ptr<Impl> i) : impl(std::move(i)) {}
};

C++20: アトミック共有ポインタアクセス

[編集]

C++20では、std::atomic<std::shared_ptr<T>>のパフォーマンスが向上し、std::atomic_*関数群も強化されました:

#include <memory>
#include <atomic>
#include <thread>

// スレッドセーフな共有リソース
class SharedResource {
private:
  std::atomic<std::shared_ptr<int>> data;

public:
  SharedResource(int value) 
    : data(std::make_shared<int>(value)) {}
  
  // C++20のアトミック操作
  void update(int new_value) {
    auto current = data.load();
    auto new_data = std::make_shared<int>(new_value);
    
    // CAS (Compare-And-Swap) パターン
    while (!data.compare_exchange_weak(current, new_data)) {
      // 他のスレッドが変更した場合は再試行
    }
  }
  
  int get() const {
    return *data.load();
  }
  
  // C++20: 待機/通知機能
  void wait_for_change(int expected) {
    data.wait([const std::shared_ptr<int>& p expected] {
      return p && *p != expected;
    });
  }
  
  void notify_all() {
    data.notify_all();
  }
};
改良点
  • std::make_sharedstd::allocate_sharedによる効率的なメモリ割り当て
  • カスタムデリータとアロケータのサポート
  • アトミック操作の強化
  • C++20のwait/notifyパターンによる効率的な同期

新規: メモリリソースとポリモーフィックアロケータ

[編集]

C++17で導入され、C++20/23で強化されたstd::pmr名前空間のコンポーネントは、カスタムメモリ管理を簡素化します。

基本的な使用パターン

[編集]
#include <memory_resource>
#include <vector>
#include <string>
#include <iostream>

void basic_pmr_example() {
  // 128KBのバッファ(スタック上)
  constexpr size_t buffer_size = 128 * 1024;
  char buffer[buffer_size];
  
  // モノトニックバッファリソース(高速な順次割り当て)
  std::pmr::monotonic_buffer_resource pool{buffer, buffer_size};
  
  // PMRベクター - カスタムメモリプールを使用
  std::pmr::vector<std::pmr::string> strings{&pool};
  
  // 文字列もプールメモリを使用
  for (int i = 0; i < 1000; ++i) {
    strings.emplace_back("String #" + std::to_string(i));
  }
  
  std::cout << "Allocated " << strings.size() << " strings\n";
  // バッファリソースの破棄時に自動的にメモリ解放
}

C++23: 高度なリソース管理パターン

[編集]
#include <memory_resource>
#include <vector>
#include <string>
#include <chrono>
#include <format>

// C++23: メモリリソース監視クラス
class monitored_resource : public std::pmr::memory_resource {
  std::pmr::memory_resource* upstream;
  std::string name;
  size_t bytes_allocated = 0;
  size_t total_allocations = 0;
  
public:
  explicit monitored_resource(std::pmr::memory_resource* up, std::string n)
    : upstream(up), name(std::move(n)) {}
    
  ~monitored_resource() {
    std::cout << std::format("[{}] 統計: {} バイト, {} 回の割り当て\n", 
                           name, bytes_allocated, total_allocations);
  }
  
private:
  void* do_allocate(size_t bytes, size_t alignment) override {
    bytes_allocated += bytes;
    ++total_allocations;
    return upstream->allocate(bytes, alignment);
  }
  
  void do_deallocate(void* p, size_t bytes, size_t alignment) override {
    bytes_allocated -= bytes;
    upstream->deallocate(p, bytes, alignment);
  }
  
  bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
    return this == &other;
  }
};

// C++23: ネストされたリソース階層
class App {
  // アプリ全体のメモリプール
  std::array<std::byte, 4 * 1024 * 1024> main_buffer;
  std::pmr::monotonic_buffer_resource main_pool{main_buffer.data(), main_buffer.size()};
  monitored_resource main_monitor{&main_pool, "アプリケーションプール"};
  
  // コンポーネント専用プール
  std::pmr::unsynchronized_pool_resource component_pool{&main_monitor};
  monitored_resource component_monitor{&component_pool, "コンポーネントプール"};
  
  // 一時オブジェクト用プール
  std::pmr::synchronized_pool_resource temp_pool{&main_monitor};
  monitored_resource temp_monitor{&temp_pool, "一時オブジェクトプール"};
  
public:
  // コンポーネント用メモリリソース取得
  std::pmr::memory_resource* get_component_resource() {
    return &component_monitor;
  }
  
  // 一時オブジェクト用メモリリソース取得
  std::pmr::memory_resource* get_temp_resource() {
    return &temp_monitor;
  }
  
  // 使用例
  void run() {
    // コンポーネントの初期化
    std::pmr::vector<std::pmr::string> permanent_data{get_component_resource()};
    
    // ワークロード実行
    for (int i = 0; i < 10; ++i) {
      std::pmr::vector<std::pmr::string> temp_data{get_temp_resource()};
      
      // 一時データ処理
      for (int j = 0; j < 1000; ++j) {
        temp_data.push_back(std::format("一時データ #{}", j));
      }
      
      // 永続データに結果を保存
      permanent_data.push_back(std::format("結果 #{}", i));
    }
    
    std::cout << "永続データサイズ: " << permanent_data.size() << "\n";
  }
};
ポリモーフィックアロケータのメリット
  • ダイナミックなメモリ管理戦略(実行時に変更可能)
  • メモリアロケーションの階層化
  • キャッシュ効率と局所性の向上
  • メモリ使用の監視と制御
  • 標準コンテナとの互換性

並行処理イディオム

[編集]

改良: Double-Checked Locking

[編集]

Double-Checked Lockingは遅延初期化パターンですが、C++11のメモリモデルでも微妙な問題がありました。C++20のstd::atomic_refとC++23の同期プリミティブでこれを改善できます。

従来のパターン(C++11)

[編集]
class Singleton {
private:
  static std::atomic<Singleton*> instance;
  static std::mutex mutex;
  
  // 他の私有データ...
  
  Singleton() { /* 初期化 */ }
  
public:
  static Singleton* getInstance() {
    Singleton* p = instance.load(std::memory_order_acquire);
    if (p == nullptr) {  // 最初のチェック
      std::lock_guard<std::mutex> lock(mutex);
      p = instance.load(std::memory_order_relaxed);
      if (p == nullptr) {  // 二重チェック
        p = new Singleton();
        instance.store(p, std::memory_order_release);
      }
    }
    return p;
  }
};

std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mutex;

C++20による改良: std::atomic_ref

[編集]
class Singleton {
private:
  class InstanceHolder {
    Singleton* ptr = nullptr;
    std::mutex mutex;
    
  public:
    Singleton* get_instance() {
      // atomic_refを使った一時的なアトミック操作
      std::atomic_ref<Singleton*> atomic_ptr(ptr);
      
      Singleton* p = atomic_ptr.load(std::memory_order_acquire);
      if (p == nullptr) {
        std::lock_guard<std::mutex> lock(mutex);
        p = atomic_ptr.load(std::memory_order_relaxed);
        if (p == nullptr) {
          p = new Singleton();
          atomic_ptr.store(p, std::memory_order_release);
        }
      }
      return p;
    }
    
    ~InstanceHolder() {
      delete ptr;
    }
  };
  
  // Meyers' Singleton for InstanceHolder
  static InstanceHolder& holder() {
    static InstanceHolder h;
    return h;
  }
  
  Singleton() = default;
  
public:
  static Singleton* getInstance() {
    return holder().get_instance();
  }
};

C++23: std::atomic_waitstd::atomic_notifyの活用

[編集]
#include <atomic>
#include <thread>
#include <optional>

template<typename T>
class LazyInit {
private:
  enum class State { Uninitialized, Initializing, Initialized };
  
  std::atomic<State> state{State::Uninitialized};
  std::optional<T> value;
  
public:
  template<typename Func>
  T& get_or_init(Func&& initializer) {
    // 初期化済みなら即座に返す
    if (state.load(std::memory_order_acquire) == State::Initialized) {
      return *value;
    }
    
    // 初期化していない場合は状態を更新
    State expected = State::Uninitialized;
    if (state.compare_exchange_strong(expected, State::Initializing,
                                     std::memory_order_acq_rel)) {
      // このスレッドが初期化を担当
      value.emplace(std::forward<Func>(initializer)());
      state.store(State::Initialized, std::memory_order_release);
      state.notify_all();  // C++23: 待機中のスレッドに通知
      return *value;
    } else {
      // 別スレッドが初期化中 - 完了を待機
      state.wait(State::Initializing);
      ///
// 別スレッドが初期化中 - 完了を待機
      state.wait(State::Initializing);  // C++23: 初期化完了まで効率的に待機
      return *value;
    }
  }
};

// 使用例
void lazy_init_example() {
  LazyInit<std::vector<int>> large_data;
  
  auto process_data = [&]() {
    // 必要になった時点で初期化
    auto& data = large_data.get_or_init([]() {
      std::vector<int> result(1000000);
      // 重い初期化処理...
      return result;
    });
    
    // データ処理...
  };
  
  // 複数スレッドから安全に使用可能
  std::vector<std::thread> threads;
  for (int i = 0; i < 10; ++i) {
    threads.emplace_back(process_data);
  }
  
  for (auto& t : threads) {
    t.join();
  }
}
C++20/23による改良点
  • std::atomic_refによる一時的なアトミック操作
  • 静的インスタンスホルダーパターンとの組み合わせ
  • C++23のwait/notifyによるブロッキングの効率化
  • 汎用的なテンプレートパターン化

新規: コルーチンによる非同期処理パターン

[編集]

C++20でコルーチンが導入され、C++23でさらに強化されました。これにより非同期処理を直感的に記述できるようになりました。

基本的なコルーチンパターン

[編集]
#include <coroutine>
#include <future>
#include <iostream>
#include <thread>

// 基本的な Future/Promise コルーチン
template<typename T>
class Task {
public:
  struct promise_type {
    std::promise<T> promise;
    
    Task get_return_object() {
      return Task(promise.get_future());
    }
    
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    
    void return_value(T value) {
      promise.set_value(std::move(value));
    }
    
    void unhandled_exception() {
      promise.set_exception(std::current_exception());
    }
  };
  
  explicit Task(std::future<T> fut) : future(std::move(fut)) {}
  
  T get() {
    return future.get();
  }
  
private:
  std::future<T> future;
};

// コルーチン実装例
Task<int> compute_value(int input) {
  // 別スレッドで重い計算を実行
  std::thread([input] {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 結果を返す場合は co_return を使用
  }).detach();
  
  co_return input * 42;  // コルーチン結果
}

C++23: 洗練されたジェネレータパターン

[編集]
#include <coroutine>
#include <iostream>
#include <optional>
#include <vector>

// C++23スタイルのジェネレータ
template<typename T>
class Generator {
public:
  struct promise_type {
    std::optional<T> current_value;
    
    Generator get_return_object() {
      return Generator{
        std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    
    std::suspend_always yield_value(T value) {
      current_value = std::move(value);
      return {};
    }
    
    void return_void() {}
    
    void unhandled_exception() {
      std::terminate();
    }
  };
  
  explicit Generator(std::coroutine_handle<promise_type> h)
    : handle(h) {}
    
  ~Generator() {
    if (handle) handle.destroy();
  }
  
  // ムーブのみ可能
  Generator(Generator&& other) noexcept : handle(other.handle) {
    other.handle = nullptr;
  }
  
  Generator& operator=(Generator&& other) noexcept {
    if (this != &other) {
      if (handle) handle.destroy();
      handle = other.handle;
      other.handle = nullptr;
    }
    return *this;
  }
  
  // イテレータインターフェース
  class iterator {
    std::coroutine_handle<promise_type> handle = nullptr;
    
  public:
    iterator() = default;
    explicit iterator(std::coroutine_handle<promise_type> h) : handle(h) {
      if (handle && !handle.done()) {
        handle.resume();  // 最初の値を生成
      }
    }
    
    iterator& operator++() {
      if (handle && !handle.done()) {
        handle.resume();  // 次の値を生成
      }
      return *this;
    }
    
    bool operator==(const iterator& other) const {
      // 終了したコルーチンかnullptrと比較した場合はtrue
      return (!handle || handle.done()) == (!other.handle || other.handle.done());
    }
    
    const T& operator*() const {
      return *handle.promise().current_value;
    }
  };
  
  iterator begin() {
    return iterator{handle};
  }
  
  iterator end() {
    return {};
  }
  
private:
  std::coroutine_handle<promise_type> handle;
};

// フィボナッチ数列ジェネレータ
Generator<int> fibonacci(int limit) {
  int a = 0, b = 1;
  
  for (int i = 0; i < limit; ++i) {
    co_yield a;
    int next = a + b;
    a = b;
    b = next;
  }
}

// 素数ジェネレータ
Generator<int> primes(int max) {
  std::vector<int> found_primes;
  
  for (int n = 2; n <= max; ++n) {
    bool is_prime = true;
    for (int p : found_primes) {
      if (p * p > n) break;
      if (n % p == 0) {
        is_prime = false;
        break;
      }
    }
    
    if (is_prime) {
      found_primes.push_back(n);
      co_yield n;
    }
  }
}

C++23: アウェイタブルによる非同期I/O連鎖

[編集]
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>

// 非同期I/Oシミュレーション用オブジェクト
class AsyncOperation {
  std::function<void()> callback;
  std::thread worker;
  bool completed = false;
  
public:
  template<typename Func>
  explicit AsyncOperation(Func operation) {
    worker = std::thread([this, op = std::move(operation)]() {
      op();
      completed = true;
      if (callback) callback();
    });
    worker.detach();
  }
  
  bool await_ready() const { return completed; }
  
  void await_suspend(std::coroutine_handle<> handle) {
    callback = [handle]() mutable {
      handle.resume();
    };
  }
  
  void await_resume() {}
};

// ファイルI/Oシミュレーション
AsyncOperation read_file(const std::string& filename) {
  return AsyncOperation([filename]() {
    std::cout << "読み込み中: " << filename << "\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "読み込み完了: " << filename << "\n";
  });
}

// ネットワークI/Oシミュレーション
AsyncOperation send_data(const std::string& url) {
  return AsyncOperation([url]() {
    std::cout << "送信中: " << url << "\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(800));
    std::cout << "送信完了: " << url << "\n";
  });
}

// タスクエグゼキュータ
class Task {
public:
  struct promise_type {
    Task get_return_object() { 
      return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
  };
  
  explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
  ~Task() { if (handle) handle.destroy(); }
  
private:
  std::coroutine_handle<promise_type> handle;
};

// C++23: 非同期タスク連鎖
Task process_document(const std::string& document) {
  std::cout << "ドキュメント処理開始: " << document << "\n";
  
  // 非同期ファイルI/O - ブロックせずに完了を待機
  co_await read_file(document);
  
  // 最初の処理
  std::cout << "データ処理中...\n";
  std::this_thread::sleep_for(std::chrono::milliseconds(300));
  
  // 複数の非同期操作を連鎖
  co_await send_data("https://api.example.com/upload");
  
  std::cout << "ドキュメント処理完了\n";
}
コルーチンによる非同期処理の利点
  • 手続き型に近い直感的なコード構造
  • コールバック地獄の回避
  • 宣言的な非同期処理フロー
  • エラー処理の簡素化
  • ジェネレータによる遅延評価と無限シーケンス

新規: 構造化並行処理

[編集]

C++20で導入されたstd::jthreadとその関連機能、およびC++23の実行モデルにより、より安全で制御しやすい並行処理が可能になりました。

C++20: std::jthreadstd::stop_token

[編集]
#include <thread>
#include <chrono>
#include <iostream>
#include <vector>
#include <atomic>

// ワーカータスク - キャンセル可能
void worker_task(std::stop_token token, int id, std::atomic<int>& counter) {
  while (!token.stop_requested()) {
    std::cout << "Worker " << id << " running...\n";
    ++counter;
    
    // キャンセル可能なスリープ
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
  }
  
  std::cout << "Worker " << id << " shutting down gracefully\n";
}

void jthread_example() {
  std::atomic<int> global_counter = 0;
  
  {
    std::cout << "Starting worker pool...\n";
    
    // ワーカースレッドプール
    std::vector<std::jthread> workers;
    
    // 複数ワーカーの起動
    for (int i = 0; i < 5; ++i) {
      workers.emplace_back(worker_task, i, std::ref(global_counter));
    }
    
    // メインスレッドの作業
    std::this_thread::sleep_for(std::chrono::seconds(3));
    
    std::cout << "Requesting shutdown...\n";
    // スコープ終了時に自動的にstop_requestとjoinが呼ばれる
  }
  
  std::cout << "All workers completed, operations count: " 
            << global_counter.load() << "\n";
}

C++23: 構造化タスク実行

[編集]
#include <execution>
#include <future>
#include <vector>
#include <iostream>
#include <chrono>
#include <random>

// C++23: スケジューラーとエグゼキュータ
class ThreadPool {
public:
  ThreadPool(size_t num_threads) {
    workers.reserve(num_threads);
    for (size_t i = 0; i < num_threads; ++i) {
      workers.emplace_back([this] {
        while (true) {
          std::function<void()> task;
          
          {
            std::unique_lock lock(queue_mutex);
            condition.wait(lock, [this] { 
              return shutdown || !tasks.empty(); 
            });
            
            if (shutdown && tasks.empty()) {
              return;
            }
            
            task = std::move(tasks.front());
            tasks.pop();
          }
          
          task();
        }
      });
    }
  }
  
  ~ThreadPool() {
    {
      std::unique_lock lock(queue_mutex);
      shutdown = true;
    }
    
    condition.notify_all();
    for (auto& worker : workers) {
      worker.join();
    }
  }
  
  template<typename F>
  auto submit(F&& f) -> std::future<decltype(f())> {
    using result_type = decltype(f());
    auto task = std::make_shared<std::packaged_task<result_type()>>(
      std::forward<F>(f)
    );
    
    std::future<result_type> result = task->get_future();
    
    {
      std::unique_lock lock(queue_mutex);
      tasks.emplace([) { (*task)(); } task];
    }
    
    condition.notify_one();
    return result;
  }
  
private:
  std::vector<std::jthread> workers;
  std::queue<std::function<void()>> tasks;
  std::mutex queue_mutex;
  std::condition_variable condition;
  bool shutdown = false;
};

// C++23: 構造化並行処理パターン
void parallel_map_example() {
  ThreadPool pool(std::thread::hardware_concurrency());
  std::vector<int> data(1000);
  
  // データ初期化
  std::mt19937 gen(42);
  std::uniform_int_distribution<> dist(1, 100);
  for (auto& x : data) {
    x = dist(gen);
  }
  
  // C++23スタイルの並行マップ処理
  auto transform = [auto input, auto transformation &pool] {
    using result_type = decltype(transformation(input[0]));
    std::vector<std::future<result_type>> futures;
    std::vector<result_type> results(input.size());
    
    // タスク分割
    for (size_t i = 0; i < input.size(); ++i) {
      futures.push_back(pool.submit([&input, &transformation, i] {
        return transformation(input[i]);
      }));
    }
    
    // 結果収集
    for (size_t i = 0; i < futures.size(); ++i) {
      results[i] = futures[i].get();
    }
    
    return results;
  };
  
  // 並列変換の実行
  auto squared = transform(data, [int x ] {
    // 計算シミュレーション
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    return x * x;
  });
  
  std::cout << "Processed " << squared.size() << " elements\n";
  double average = 0.0;
  for (auto x : squared) {
    average += x;
  }
  average /= squared.size();
  
  std::cout << "平均値: " << average << "\n";
}
構造化並行処理の利点
  • リソース管理の自動化 (std::jthreadのスコープベース停止)
  • 明示的なキャンセルメカニズム (std::stop_token)
  • 効率的なスレッドプール実装
  • 高レベルな並列アルゴリズムパターン
  • 実行コンテキストの分離

テンプレートイディオム

[編集]

改良: Curiously Recurring Template Pattern (CRTP)

[編集]

CRTP(奇妙に再帰するテンプレートパターン)は、静的ポリモーフィズムを実現する古典的なC++イディオムですが、C++20のコンセプトを活用することで、より安全で表現力豊かに進化しました。

コンセプトによる制約の追加

[編集]

従来のCRTPでは、基底クラスと派生クラスの関係性が暗黙的でした。C++20のコンセプトを使うことで、この関係を明示的に表現し、コンパイル時に検証できます。

// 従来のCRTP
template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Derived implementation\n";
    }
};
コンセプトを活用した改良版:
// C++20コンセプトを使用したCRTP
template <typename T>
concept Implementor = requires(T t) {
    { t.implementation() } -> std::same_as<void>;
};

template <Implementor Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Improved implementation with concepts\n";
    }
};
変更ポイント
  • Implementorコンセプトによって、派生クラスが実装すべきメソッドを明示的に定義
  • コンパイル時にインターフェース違反を検出
  • APIドキュメントとしての役割も果たす
CRTPのもう一つの一般的なユースケースは、演算子の自動実装です。これもコンセプトで制約できます:
template <typename T>
concept Comparable = requires(T a, T b) {
    { a.compare(b) } -> std::convertible_to<int>;
};

template <Comparable Derived>
class ComparisonBase {
public:
    friend bool operator<(const Derived& a, const Derived& b) {
        return a.compare(b) < 0;
    }
    
    friend bool operator==(const Derived& a, const Derived& b) {
        return a.compare(b) == 0;
    }
    
    // C++20では他の比較演算子は自動的に生成されます
};

class Number : public ComparisonBase<Number> {
    int value;
public:
    Number(int v) : value(v) {}
    
    int compare(const Number& other) const {
        return value - other.value;
    }
};

C++20のテンプレートラムダとの連携

[編集]

C++20からテンプレートラムダが導入されたことで、CRTPパターンをより柔軟に活用できるようになりました。

#include <iostream>
#include <concepts>
#include <functional>

template <typename Derived>
concept Printable = requires(Derived d) {
    { d.print() } -> std::same_as<void>;
};

// テンプレートラムダを使用したCRTPヘルパー
auto make_printer = []<Printable T>(T& obj) {
    return [&obj]() {
        std::cout << "Printing via template lambda: ";
        obj.print();
    };
};

class Document : public std::enable_shared_from_this<Document> {
public:
    void print() {
        std::cout << "Document contents\n";
    }
    
    auto get_printer() {
        return make_printer(*this);
    }
};
変更ポイント
  • テンプレートラムダによりCRTPのコンテキスト外でも型安全な操作が可能に
  • std::enable_shared_from_thisなどの既存CRTPパターンとの連携が容易に
  • よりモジュール化されたコード設計が可能に

改良: Expression Templates

[編集]

式テンプレートは数値計算ライブラリなどで一時オブジェクトの生成を避け、最適化された計算を実現するためのテクニックです。C++20のコンセプトとコンパイル時評価機能により、より安全で効率的になりました。

C++20のコンセプトを使った安全な実装

[編集]

従来の式テンプレートでは型安全性に欠ける部分がありましたが、コンセプトを用いることで実装がより明確になります。

// 従来の式テンプレート
template <typename LHS, typename RHS>
class VectorAddition {
    const LHS& lhs;
    const RHS& rhs;
public:
    VectorAddition(const LHS& l, const RHS& r) : lhs(l), rhs(r) {}
    
    double operator[size_t i ] const {
        return lhs[i] + rhs[i];
    }
    
    size_t size() const {
        return lhs.size();
    }
};

C++20のコンセプトを活用した改良版:

#include <concepts>
#include <vector>

// コンセプトによる要件定義
template <typename T>
concept VectorExpression = requires(T v, size_t i) {
    { v[i] } -> std::convertible_to<double>;
    { v.size() } -> std::convertible_to<size_t>;
};

// 式テンプレート
template <VectorExpression LHS, VectorExpression RHS>
class VectorAddition {
    const LHS& lhs;
    const RHS& rhs;
public:
    VectorAddition(const LHS& l, const RHS& r) : lhs(l), rhs(r) {
        if constexpr (std::is_same_v<LHS, std::vector<double>> && 
                      std::is_same_v<RHS, std::vector<double>>) {
            if (l.size() != r.size()) {
                throw std::length_error("Vector size mismatch");
            }
        }
    }
    
    double operator[size_t i ] const {
        return lhs[i] + rhs[i];
    }
    
    size_t size() const {
        return lhs.size();
    }
};

// 演算子オーバーロード
template <VectorExpression LHS, VectorExpression RHS>
VectorAddition<LHS, RHS> operator+(const LHS& lhs, const RHS& rhs) {
    return VectorAddition<LHS, RHS>(lhs, rhs);
}
変更ポイント
  • VectorExpressionコンセプトによる型制約
  • if constexprによる条件付きコンパイル
  • 実行時およびコンパイル時の安全性チェック

コンパイル時評価の強化

[編集]

C++20のconstevalconstexprの機能強化により、式テンプレートの評価をコンパイル時に行える範囲が広がりました。

#include <array>
#include <concepts>

template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

template <Numeric T, size_t N>
class Vector {
    std::array<T, N> data;

public:
    constexpr Vector() : data{} {}
    
    template <typename... Args>
    requires (sizeof...(Args) == N)
    constexpr Vector(Args... args) : data{static_cast<T>(args)...} {}
    
    constexpr T operator[size_t i ] const {
        return data[i];
    }
    
    constexpr T& operator[size_t i ] {
        return data[i];
    }
    
    static constexpr size_t size() {
        return N;
    }
};

// 式テンプレート
template <Numeric T, size_t N, typename Expr>
requires VectorExpression<Expr>
class VectorExpr {
    const Expr& expr;
    
public:
    constexpr VectorExpr(const Expr& e) : expr(e) {}
    
    constexpr T operator[size_t i ] const {
        return expr[i];
    }
    
    static constexpr size_t size() {
        return N;
    }
    
    // コンパイル時評価可能な変換演算子
    constexpr operator Vector<T, N>() const {
        Vector<T, N> result;
        for (size_t i = 0; i < N; ++i) {
            result[i] = (*this)[i];
        }
        return result;
    }
};

// コンパイル時に評価される関数
consteval auto compute_vector_sum() {
    Vector<int, 3> v1{1, 2, 3};
    Vector<int, 3> v2{4, 5, 6};
    auto expr = v1 + v2;  // 式テンプレート
    return static_cast<Vector<int, 3>>(expr);  // コンパイル時に評価
}
変更ポイント
  • constexprconstevalによる完全なコンパイル時評価
  • サイズ情報を型に含めることによる追加の安全性
  • requires句による制約の明示

新規: コンパイル時リフレクション

[編集]

C++23で導入された静的リフレクション機能は、コンパイル時にプログラム構造を分析し操作する強力なツールです。これをテンプレートと組み合わせることで、新しいイディオムが生まれています。

C++23の静的リフレクション機能

[編集]

C++23の静的リフレクションは、std::meta名前空間を通じて提供されます(注:標準化プロセスにより、実際の実装詳細は変更される可能性があります)。

#include <iostream>
#include <meta>

struct Person {
    std::string name;
    int age;
    double height;
};

// リフレクションを使った自動シリアライズ関数
template <typename T>
std::string to_json(const T& obj) {
    std::string result = "{";
    
    // クラスのメンバーを反復処理
    constexpr auto members = std::meta::members_of(std::meta::type_of<T>);
    bool first = true;
    
    std::meta::for_each<members>([auto member &] {
        if (!first) result += ",";
        first = false;
        
        // メンバー名の取得
        constexpr auto name = std::meta::name_of(member);
        result += "\"" + std::string(name) + "\":";
        
        // メンバー値の取得とシリアライズ
        if constexpr (std::is_same_v<std::meta::type_of(member), std::string>) {
            result += "\"" + std::meta::value_of(obj.*member) + "\"";
        } else {
            result += std::to_string(std::meta::value_of(obj.*member));
        }
    });
    
    result += "}";
    return result;
}
変更ポイント
  • コンパイル時にクラス構造を分析
  • リフレクションによる自動コード生成
  • テンプレートメタプログラミングからリフレクションへの移行

メタプログラミングと連携したリフレクション活用パターン

[編集]

リフレクションはバリデーション、シリアライゼーション、オブジェクトマッピングなど様々な用途に活用できます。

#include <iostream>
#include <meta>
#include <type_traits>
#include <string>
#include <vector>

// アノテーションを表現する属性
struct [[attribute]] Required {};
struct [[attribute]] Range { int min; int max; };

// リフレクションとバリデーションを組み合わせたクラス
class UserProfile {
public:
    [[Required]] std::string username;
    [[Required]] std::string email;
    [[Range(13, 120)]] int age;
    std::vector<std::string> interests;
    
    // リフレクションを使った自動バリデーション
    std::vector<std::string> validate() const {
        std::vector<std::string> errors;
        
        std::meta::for_each<std::meta::members_of(std::meta::type_of<UserProfile>)>(
            [auto member &] {
                constexpr auto name = std::meta::name_of(member);
                constexpr auto attrs = std::meta::attributes_of(member);
                
                // Requiredチェック
                if constexpr (std::meta::contains<attrs, Required>()) {
                    const auto& value = this->*member;
                    if constexpr (std::is_same_v<std::decay_t<decltype(value)>, std::string>) {
                        if (value.empty()) {
                            errors.push_back(std::string(name) + " is required");
                        }
                    }
                }
                
                // Range制約チェック
                if constexpr (std::meta::contains<attrs, Range>()) {
                    constexpr auto range_attr = std::meta::get<Range, attrs>();
                    const auto& value = this->*member;
                    if (value < range_attr.min || value > range_attr.max) {
                        errors.push_back(std::string(name) + " must be between " + 
                                       std::to_string(range_attr.min) + " and " + 
                                       std::to_string(range_attr.max));
                    }
                }
            }
        );
        
        return errors;
    }
};

// リフレクションを用いた自動テーブルマッピング
template <typename T>
class TableMapper {
public:
    static std::string create_table_sql() {
        std::string sql = "CREATE TABLE " + std::string(std::meta::name_of(std::meta::type_of<T>)) + " (";
        
        bool first = true;
        std::meta::for_each<std::meta::members_of(std::meta::type_of<T>)>(
            [auto member &] {
                if (!first) sql += ", ";
                first = false;
                
                sql += std::string(std::meta::name_of(member)) + " ";
                
                // 型に基づいたSQL型の決定
                using MemberType = std::decay_t<decltype(std::declval<T>().*member)>;
                if constexpr (std::is_same_v<MemberType, int>) {
                    sql += "INTEGER";
                } else if constexpr (std::is_same_v<MemberType, double>) {
                    sql += "REAL";
                } else if constexpr (std::is_same_v<MemberType, std::string>) {
                    sql += "TEXT";
                } else {
                    sql += "BLOB";
                }
                
                // 属性から制約を追加
                constexpr auto attrs = std::meta::attributes_of(member);
                if constexpr (std::meta::contains<attrs, Required>()) {
                    sql += " NOT NULL";
                }
            }
        );
        
        sql += ");";
        return sql;
    }
};
変更ポイント
  • 属性(アノテーション)とリフレクションの組み合わせ
  • コンパイル時バリデーションルールの適用
  • データベースマッピングなどのボイラープレートコードの自動生成
  • 型安全なリフレクション操作

このようにC++20/23のテンプレートイディオムは、コンセプト、コンパイル時評価、リフレクションといった新機能を駆使することで、より安全で表現力が高く、メンテナンス性に優れたコードを実現しています。特にリフレクションの導入は、C++のメタプログラミングを根本から変え、従来は不可能だった多くのユースケースを現実的なものにしています。

ユーティリティイディオム

[編集]

改良: Type Erasure

[編集]

Type Erasureは型の詳細を隠蔽し、共通インターフェースを通じて異種の型を扱えるようにするテクニックです。C++23では新たな機能追加により、より強力で使いやすくなりました。

C++23のstd::any_invocableを活用した実装

[編集]

従来のType Erasureは実装が複雑で、特に関数オブジェクトに対してはstd::functionに制約がありました。C++23で導入されたstd::any_invocableはこの問題を解決します。

#include <functional>
#include <memory>
#include <iostream>
#include <string>

// C++23の std::any_invocable を使用したタイプイレージャー
class MessageHandler {
private:
    std::any_invocable<void(const std::string&)> m_handler;

public:
    // 任意の呼び出し可能オブジェクトを受け入れるコンストラクタ
    template <typename Callable>
    MessageHandler(Callable&& handler) 
        : m_handler(std::forward<Callable>(handler)) {}
    
    // ムーブコンストラクタとムーブ代入は自動的に正しく動作
    MessageHandler(MessageHandler&&) = default;
    MessageHandler& operator=(MessageHandler&&) = default;
    
    // コピー不可(std::any_invocableはムーブオンリー)
    MessageHandler(const MessageHandler&) = delete;
    MessageHandler& operator=(const MessageHandler&) = delete;
    
    // メッセージを処理
    void handle(const std::string& message) {
        m_handler(message);
    }
};

// 使用例
void test_message_handler() {
    // ラムダを使用
    MessageHandler console_logger([const std::string& msg ] {
        std::cout << "Log: " << msg << std::endl;
    });
    
    // 関数オブジェクトを使用
    struct EmailSender {
        void operator()(const std::string& msg) const {
            std::cout << "Sending email: " << msg << std::endl;
        }
    };
    MessageHandler email_notifier(EmailSender{});
    
    // 通常関数を使用
    auto sms_notifier = [const std::string& msg ] {
        std::cout << "Sending SMS: " << msg << std::endl;
    };
    MessageHandler sms_handler(sms_notifier);
    
    // 使用
    console_logger.handle("Application started");
    email_notifier.handle("Critical error occurred");
    sms_handler.handle("System alert");
}
変更ポイント
  • std::any_invocableはムーブオンリーなので、ステートフルラムダやユニークリソースを持つ呼び出し可能オブジェクトを効率的に扱える
  • 従来のstd::functionと異なり、コピー不可能な関数オブジェクトもサポート
  • パフォーマンスオーバーヘッドが低減

コンセプトとの統合

[編集]

C++20のコンセプトを活用することで、Type Erasureの使用方法をより明確にし、制約を追加できます。

#include <concepts>
#include <memory>
#include <string>
#include <vector>

// 描画可能なオブジェクトの要件を定義するコンセプト
template <typename T>
concept Drawable = requires(T& t, std::ostream& os) {
    { t.draw(os) } -> std::same_as<void>;
    { t.name() } -> std::convertible_to<std::string>;
};

// Type Erasureを実装した描画可能オブジェクトコンテナ
class DrawableObject {
private:
    struct Concept {
        virtual ~Concept() = default;
        virtual void draw(std::ostream& os) const = 0;
        virtual std::string name() const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };
    
    template <Drawable T>
    struct Model : Concept {
        T object;
        
        explicit Model(T obj) : object(std::move(obj)) {}
        
        void draw(std::ostream& os) const override {
            object.draw(os);
        }
        
        std::string name() const override {
            return object.name();
        }
        
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model<T>>(object);
        }
    };
    
    std::unique_ptr<Concept> m_pimpl;
    
public:
    // コンセプト制約付きコンストラクタ
    template <Drawable T>
    DrawableObject(T obj)
        : m_pimpl(std::make_unique<Model<T>>(std::move(obj))) {}
    
    // 特殊メンバ関数
    DrawableObject(const DrawableObject& other)
        : m_pimpl(other.m_pimpl ? other.m_pimpl->clone() : nullptr) {}
    
    DrawableObject& operator=(const DrawableObject& other) {
        if (this != &other) {
            m_pimpl = other.m_pimpl ? other.m_pimpl->clone() : nullptr;
        }
        return *this;
    }
    
    DrawableObject(DrawableObject&&) = default;
    DrawableObject& operator=(DrawableObject&&) = default;
    
    // インターフェースメソッド
    void draw(std::ostream& os) const {
        if (m_pimpl) m_pimpl->draw(os);
    }
    
    std::string name() const {
        return m_pimpl ? m_pimpl->name() : "null";
    }
};

// 使用例
struct Circle {
    double radius;
    
    void draw(std::ostream& os) const {
        os << "Circle(radius=" << radius << ")";
    }
    
    std::string name() const {
        return "Circle";
    }
};

struct Rectangle {
    double width, height;
    
    void draw(std::ostream& os) const {
        os << "Rectangle(" << width << "x" << height << ")";
    }
    
    std::string name() const {
        return "Rectangle";
    }
};
変更ポイント
  • Drawableコンセプトによる明示的なインターフェース定義
  • コンパイル時の制約チェックによる安全性向上
  • テンプレートエラーメッセージの改善と開発者体験の向上

新規: std::formatと文字列処理パターン

[編集]

C++20で導入されたstd::formatライブラリは、従来の文字列フォーマット方法に代わる、型安全で柔軟な機能を提供します。C++23ではさらに機能拡張されています。

従来の文字列操作イディオムの置き換え

[編集]
#include <format>
#include <string>
#include <iostream>
#include <chrono>

// 従来の方法(C++17以前)
std::string legacy_format(const std::string& name, int age, double height) {
    std::ostringstream oss;
    oss << "Name: " << name << ", Age: " << age << ", Height: " 
        << std::fixed << std::setprecision(1) << height << "m";
    return oss.str();
}

// C++20/23 std::formatを使用
std::string modern_format(const std::string& name, int age, double height) {
    return std::format("Name: {}, Age: {}, Height: {:.1f}m", name, age, height);
}

// カスタム型のフォーマットサポート
struct Person {
    std::string name;
    int age;
    double height;
};

// C++20: std::formatter特殊化によるカスタムフォーマット
template <>
struct std::formatter<Person> {
    constexpr auto parse(std::format_parse_context& ctx) {
        return ctx.begin();
    }
    
    auto format(const Person& p, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "Person({}, {} years, {:.1f}m)",
                             p.name, p.age, p.height);
    }
};

// C++23: フォーマット文字列の検証
consteval auto validate_person_format() {
    Person test{"John", 30, 1.75};
    // コンパイル時に書式文字列を検証
    constexpr auto fmt = std::format("Profile: {}", test);
    return true;
}

// C++23: 日時フォーマットの改良
void print_date_time() {
    auto now = std::chrono::system_clock::now();
    auto local_time = std::chrono::current_zone()->to_local(now);
    
    // C++23の拡張chrono書式
    std::cout << std::format("Current date: {:%Y-%m-%d}", local_time) << std::endl;
    std::cout << std::format("Current time: {:%H:%M:%S %Z}", local_time) << std::endl;
    std::cout << std::format("Locale-specific: {:%x %X}", local_time) << std::endl;
}
変更ポイント
  • Pythonのformat関数に似た型安全なフォーマット構文
  • カスタム型のフォーマット対応が簡単
  • C++23でのコンパイル時フォーマット検証
  • 日時フォーマットの標準化と拡張

C++23のフォーマット仕様の活用

[編集]

C++23ではstd::formatがさらに強化され、std::printが導入されています。

#include <format>
#include <print>
#include <string_view>
#include <map>
#include <vector>

// C++23: std::printの活用
void demo_print() {
    std::print("Hello, {}!\n", "world");  // 直接出力
    
    // フォーマットされた結果を変数に格納せずに直接出力
    int count = 42;
    double value = 3.14159;
    std::print("Count: {}, Value: {:.2f}\n", count, value);
    
    // Unicode対応
    std::print("円周率: {:.5f}\n", 3.14159);
}

// C++23: fill, align, widthの高度な使用
void advanced_formatting() {
    // テーブル形式の出力(幅指定と揃え)
    std::vector<std::pair<std::string, int>> scores = {
        {"Alice", 95},
        {"Bob", 87},
        {"Charlie", 92}
    };
    
    std::print("{:=^50}\n", " SCOREBOARD ");
    for (const auto& [name, score] : scores) {
        std::print("{:<30}|{:>8}\n", name, score);
    }
    std::print("{:=^50}\n", "");
    
    // 数値フォーマット(桁区切り、基数、前置詞)
    int large_number = 1'234'567;
    std::print("Decimal:    {:n}\n", large_number);
    std::print("Hexadecimal: {:#x}\n", large_number);
    std::print("Binary:      {:#b}\n", large_number);
    
    // 計算内のフォーマット
    double a = 10.5, b = 20.7;
    std::print("Sum: {}\n", std::format("{:.1f} + {:.1f} = {:.2f}", a, b, a + b));
}

// C++23: フォーマット仕様による構造化データの出力
void format_structured_data() {
    std::map<std::string, std::vector<int>> data = {
        {"Sensor1", {10, 20, 15, 30}},
        {"Sensor2", {22, 18, 25}},
        {"Sensor3", {5, 15, 10, 12, 8}}
    };
    
    // JSON風のフォーマット
    std::print("{{\n");
    for (const auto& [key, values] : data) {
        std::print("  {}: [", key);
        for (size_t i = 0; i < values.size(); ++i) {
            std::print("{}{}", values[i], i < values.size() - 1 ? ", " : "");
        }
        std::print("],\n");
    }
    std::print("}}\n");
}
変更ポイント
  • std::printによる出力の簡素化
  • 高度なフォーマットオプション(幅、揃え、塗りつぶし文字)
  • 国際化対応の数値フォーマット
  • 構造化データの効率的な出力

新規: 範囲とビュー

[編集]

C++20で導入された範囲ライブラリ(Rangesライブラリ)は、コレクションを扱う方法を根本から変え、C++23ではさらに機能拡張されています。

C++20の範囲ライブラリのイディオム

[編集]
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>

// 基本的な範囲操作
void basic_ranges_demo() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // フィルタと変換を組み合わせる
    auto even_squares = nums |
                        std::views::filter([int n) { return n % 2 == 0; } ]
                        std::views::transform([int n) { return n * n; } ];
    
    std::cout << "Even squares: ";
    for (int value : even_squares) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    // テイクとドロップを使用
    auto middle_elements = nums | std::views::drop(2) | std::views::take(5);
    std::cout << "Middle elements: ";
    for (int value : middle_elements) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

// 複雑なデータ処理パイプライン
void advanced_ranges_demo() {
    std::vector<std::string> names = {
        "Alice Smith", "Bob Johnson", "Charlie Williams",
        "David Brown", "Eve Davis", "Frank Miller"
    };
    
    // 名前を処理するパイプライン
    auto processed_names = names
        // 6文字以上の名前だけをフィルタ
        std::views::filter([const std::string& name) { return name.length() > 10; } ]
        // 空白で分割
        std::views::transform([const std::string& full_name ] {
            auto space_pos = full_name.find(' ');
            return std::make_pair(
                full_name.substr(0, space_pos),                   // 名
                full_name.substr(space_pos + 1)                   // 姓
            );
        })
        // 姓が "B" から始まるものだけをフィルタ
        std::views::filter([const auto& name_pair ] {
            return name_pair.second[0] == 'B' || name_pair.second[0] == 'W';
        });
    
    std::cout << "Filtered names:" << std::endl;
    for (const auto& [first, last] : processed_names) {
        std::cout << last << ", " << first << std::endl;
    }
}

// カスタムビューアダプタ
namespace custom_views {
    // 範囲内の要素のインデックスと値のペアを生成するビュー
    template <std::ranges::input_range R>
    class enumerate_view : public std::ranges::view_interface<enumerate_view<R>> {
    private:
        R base_;
        
    public:
        struct iterator {
            using base_iterator = std::ranges::iterator_t<R>;
            base_iterator current;
            size_t index = 0;
            
            auto operator*() const {
                return std::make_pair(index, *current);
            }
            
            iterator& operator++() {
                ++current;
                ++index;
                return *this;
            }
            
            iterator operator++(int) {
                auto tmp = *this;
                ++*this;
                return tmp;
            }
            
            bool operator==(const iterator& other) const {
                return current == other.current;
            }
        };
        
        explicit enumerate_view(R base) : base_(std::move(base)) {}
        
        auto begin() {
            return iterator{std::ranges::begin(base_), 0};
        }
        
        auto end() {
            return iterator{std::ranges::end(base_), 0};
        }
    };
    
    // enumerate関数
    template <std::ranges::input_range R>
    auto enumerate(R&& r) {
        return enumerate_view<std::views::all_t<R>>(std::views::all(std::forward<R>(r)));
    }
}
変更ポイント
  • パイプラインスタイルによる宣言的なデータ処理
  • 遅延評価による効率的な処理
  • 複雑な処理をわかりやすく表現できる
  • カスタムビューアダプタの実装が可能

C++23のレンジ拡張機能とパイプライン処理

[編集]

C++23では、レンジライブラリがさらに強化され、より多くのビュータイプや操作が提供されています。

#include <ranges>
#include <algorithm>
#include <string>
#include <vector>
#include <map>
#include <iostream>

// C++23: ジップビューとその活用
void zip_view_demo() {
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
    std::vector<int> ages = {25, 34, 28};
    std::vector<std::string> cities = {"New York", "London", "Tokyo", "Paris"};
    
    // 複数の範囲を同時に走査
    auto people = std::views::zip(names, ages);
    for (const auto& [name, age] : people) {
        std::cout << name << " is " << age << " years old." << std::endl;
    }
    
    // zip_transformを使用した値の直接変換
    auto formatted_people = std::views::zip_transform(
        [const std::string& name, int age ] {
            return std::format("{} ({})", name, age);
        },
        names, ages
    );
    
    std::cout << "\nFormatted people:" << std::endl;
    for (const auto& person : formatted_people) {
        std::cout << person << std::endl;
    }
    
    // C++23: 異なる長さの範囲のジップを扱う
    auto zipped_with_cities = std::views::zip(names, ages, cities);
    std::cout << "\nPeople with cities:" << std::endl;
    for (const auto& [name, age, city] : zipped_with_cities) {
        std::cout << name << " (" << age << ") lives in " << city << std::endl;
    }
}

// C++23: アダプタの新機能
void ranges_adaptors_demo() {
    std::vector<int> values = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    
    // chunk: 指定サイズのグループに分割
    auto chunks = values | std::views::chunk(3);
    std::cout << "Chunks:" << std::endl;
    for (const auto& chunk : chunks) {
        std::cout << "  ";
        for (int v : chunk) {
            std::cout << v << " ";
        }
        std::cout << std::endl;
    }
    
    // slide: 指定サイズの連続するウィンドウを作成
    auto windows = values | std::views::slide(4);
    std::cout << "\nSliding windows:" << std::endl;
    for (const auto& window : windows) {
        std::cout << "  ";
        for (int v : window) {
            std::cout << v << " ";
        }
        std::cout << std::endl;
    }
    
    // C++23: 一意の要素
    auto unique_values = values | std::views::unique;
    std::cout << "\nUnique values: ";
    for (int v : unique_values) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
    
    // C++23: 辞書 (join_with)
    std::vector<std::pair<std::string, int>> items = {
        {"apple", 5}, {"banana", 3}, {"orange", 7}
    };
    
    auto item_strings = items | std::views::transform([const auto& pair ] {
        return std::format("{}={}", pair.first, pair.second);
    });
    
    // join_withを使用して区切り文字で結合
    auto joined = item_strings | std::views::join_with(", ");
    std::cout << "\nItems: ";
    for (char c : joined) {
        std::cout << c;
    }
    std::cout << std::endl;
}

// C++23: 完全なデータ処理パイプラインの例
void complete_pipeline_demo() {
    struct Transaction {
        std::string customer;
        std::string product;
        double amount;
        std::string date;
    };
    
    std::vector<Transaction> transactions = {
        {"John", "Laptop", 1299.99, "2023-01-15"},
        {"Mary", "Phone", 899.50, "2023-01-20"},
        {"John", "Headphones", 149.99, "2023-01-25"},
        {"Sarah", "Tablet", 549.99, "2023-01-30"},
        {"Mary", "Charger", 29.99, "2023-02-05"},
        {"John", "Mouse", 59.99, "2023-02-10"}
    };
    
    // 高度な処理パイプライン
    auto john_transactions = transactions
        // Johnの取引のみにフィルタ
        std::views::filter([const Transaction& t) { return t.customer == "John"; } ]
        // 金額と商品名の取得
        std::views::transform([const Transaction& t ] {
            return std::make_pair(t.amount, t.product);
        })
        // 金額で並べ替え (C++23)
        std::views::sort([const auto& a, const auto& b ] {
            return a.first > b.first;  // 降順
        });
    
    double total = 0.0;
    std::cout << "John's transactions (highest first):" << std::endl;
    for (const auto& [amount, product] : john_transactions) {
        std::cout << std::format("{:<15} ${:.2f}", product, amount) << std::endl;
        total += amount;
    }
    std::cout << std::format("Total spent: ${:.2f}", total) << std::endl;
    
    // グループ化と集計 (C++23)
    std::map<std::string, double> totals_by_customer;
    for (const auto& t : transactions) {
        totals_by_customer[t.customer] += t.amount;
    }
    
    auto customer_summary = std::views::all(totals_by_customer)
        std::views::transform([const auto& pair ] {
            auto [customer, total] = pair;
            return std::make_pair(customer, total);
        })
        std::views::sort([const auto& a, const auto& b ] {
            return a.second > b.second;  // 総額で降順ソート
        });
    
    std::cout << "\nCustomer spending summary:" << std::endl;
    for (const auto& [customer, total] : customer_summary) {
        std::cout << std::format("{:<10} ${:.2f}", customer, total) << std::endl;
    }
}
変更ポイント
  • zipzip_transformによる複数範囲の同時処理
  • chunkslideによる高度なデータ分割
  • uniqueによる重複除去
  • join_withによる要素の結合
  • C++23のsortによるランタイムソート
  • 複雑なデータ処理パイプラインの構築

以上のユーティリティイディオムは、C++20/23の新機能を活用することで、より読みやすく、メンテナンスしやすく、そして効率的なコードを書くための基盤を提供します。Type Erasureはインターフェース抽象化を、std::formatは文字列処理を、そしてRangesは複雑なデータ処理をそれぞれ大幅に改善しています。