はじめに
C++プログラミングでは、効率的な実行速度とプログラムの応答性を高めるために並列処理が非常に重要です。
特に、大規模なデータを扱うアプリケーションやリアルタイム処理が求められるシステム開発において、並列処理の知識と技術は不可欠です。
本記事では、C++における並列処理の基本から応用までを詳細に解説し、実践的なプログラミング技術を身につけるための助けとなることを目指します。
●C++における並列処理の基本
並列処理とは、複数の計算処理を同時に実行することで、プログラムの実行効率を向上させる技術です。
C++では、スレッドやマルチスレッディングを通じてこの並列処理を実現します。
これにより、CPUの複数のコアを効率的に活用し、処理速度の向上を図ることができます。
○並列処理とは何か?
並列処理は、複数のプロセスまたはスレッドが同時に異なるタスクを実行することを指します。
これは、特に複雑な計算や大量のデータ処理が必要なアプリケーションでのパフォーマンス向上に寄与します。
単一のプロセスで逐次的にタスクを実行するのではなく、複数のプロセスやスレッドが並行して作業を進めることで、全体の処理時間を大幅に短縮できる可能性があります。
○C++での並列処理の重要性
C++での並列処理は、マルチコアプロセッサの普及に伴い、より一層の重要性を帯びています。
マルチコアプロセッサをフルに活用するためには、並列処理が不可欠です。
C++では、標準ライブラリに含まれるスレッド関連の機能を用いて、比較的簡単に並列処理を実装することが可能です。
これにより、アプリケーションのレスポンスの改善、リアルタイム処理の強化、計算時間の短縮など、多くのメリットを享受することができます。
C++での並列処理は、特に大規模なデータ処理やリアルタイムシステム、高性能計算などの分野でその価値を発揮します。
マルチスレッディングを利用することで、データベース操作、ファイル処理、ネットワーク通信など、さまざまなタスクを効率的に処理することができます。
また、並列処理はプログラムの設計を複雑にすることもありますが、C++の言語機能と標準ライブラリを適切に利用することで、この複雑さを管理し、効率的なプログラムを作成することが可能です。
●並列処理の基本概念と用語
並列処理は、複数の処理を同時に実行することを指し、効率的なプログラミングのために重要な概念です。
並列処理の実現には、複数のプロセスまたはスレッドを利用します。C++では、これらの概念が重要な役割を果たします。
プロセスとは、実行中のプログラムのインスタンスであり、独自のメモリ空間を持っています。
一方、スレッドはプロセス内で実行される実行の単位で、プロセスのメモリ空間を共有します。
この分野では、いくつかの基本的な用語が頻繁に使用されます。
例えば、並行性(Concurrency)は、複数の処理が同時に行われることを意味しますが、必ずしも同時に実行されるわけではありません。
これに対し、並列性(Parallelism)は、複数の処理が物理的に同時に実行される状態を指します。
また、非同期処理(Asynchronous processing)は、一つの処理が完了するのを待たずに次の処理を開始する手法です。
C++では、これらの概念を実現するために様々なライブラリや機能が提供されています。
例えば、C++11からは、スレッドをより簡単に扱うことができるstd::threadライブラリが導入されました。
また、非同期処理をサポートするために、std::asyncやstd::futureなどの機能も提供されています。
○スレッドとは?
スレッドは、プログラム内での実行の流れの単位です。
一つのプロセスは、一つ以上のスレッドを持つことができ、各スレッドはプロセスのリソースを共有しながら独立して実行されます。
これにより、複数の作業を並行して行うことができ、プログラムの効率が向上します。
C++では、std::threadライブラリを使用してスレッドを作成し、管理することができます。
このライブラリを利用することで、スレッドの作成、実行、終了の処理を簡潔に記述することが可能です。
スレッドの使用にはいくつかの注意点があります。
例えば、共有リソースへのアクセスには注意が必要です。
複数のスレッドが同時に同じリソースにアクセスすると、データの不整合が発生する可能性があります。
この問題を避けるために、ミューテックス(Mutex)やセマフォ(Semaphore)などの同期機構を利用する必要があります。
○マルチスレッディングと並列処理
マルチスレッディングは、複数のスレッドを使用して並行処理を実現する手法です。
この手法により、プログラムは複数のタスクを同時に処理できるようになり、全体の処理速度が向上します。
特に、マルチコアプロセッサを搭載したシステムでは、並列処理によって各コアが別々のタスクを同時に実行することで、大幅なパフォーマンス向上が期待できます。
C++では、マルチスレッディングを実現するために、標準ライブラリの機能やサードパーティライブラリを利用することができます。
しかし、マルチスレッディングの実装は複雑であり、スレッド間のデータの共有や同期に関する問題に注意を払う必要があります。
正確な設計とテストを行うことで、これらの問題を最小限に抑えることが可能です。
●C++での並列処理の実装
C++での並列処理の実装は、プログラムの効率を大幅に向上させる可能性があります。
特に、マルチスレッドプログラミングは、多くの現代的なアプリケーションにおいて重要な役割を果たします。
C++11以降の標準では、スレッドの作成や管理を容易にするための多くの機能が導入されています。
これらの機能を利用することで、複雑な並列処理のコードをより簡単に、そして安全に記述することができます。
○サンプルコード1:基本的なスレッドの作成
C++でスレッドを作成する基本的な方法は、std::threadクラスを使用することです。
このクラスを利用することで、新しいスレッドを生成し、それに実行すべきタスクを割り当てることができます。
#include <iostream>
#include <thread>
void task() {
std::cout << "スレッドからの出力" << std::endl;
}
int main() {
std::thread t(task);
t.join(); // スレッドの終了を待つ
return 0;
}
このコードでは、task
という関数を別のスレッドで実行しています。std::thread
のコンストラクタにその関数を渡すことで、新しいスレッドが開始されます。
join()
メソッドはメインスレッドが、新しく作成したスレッドの終了を待つために使用されます。
○サンプルコード2:スレッド間のデータ共有
スレッド間でデータを共有する際には、データの整合性を保つために注意が必要です。
ここでは、ミューテックスを使用してスレッド間でデータを安全に共有する方法を紹介します。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void increment() {
mtx.lock();
++shared_data;
mtx.unlock();
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "共有データ: " << shared_data << std::endl;
return 0;
}
この例では、increment
関数が同時に複数のスレッドから呼び出されても、mtx
(ミューテックス)によってデータへのアクセスが同期されます。
これにより、データの不整合を防ぐことができます。
○サンプルコード3:スレッドの同期化
スレッド間の正確な同期は、並列処理において非常に重要です。
ここでは、条件変数を使用してスレッドを同期する方法を紹介します。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
std::cout << "スレッド " << id << std::endl;
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10スレッドが準備完了..." << std::endl;
go(); // スレッドを起動
for (auto& th : threads) th.join();
return 0;
}
この例では、10個のスレッドがprint_id
関数を実行しますが、ready
変数がtrueになるまで待機します。
go
関数が呼ばれるとready
がtrueに設定され、条件変数により待機しているスレッドが起動します。
○サンプルコード4:スレッドの終了処理
スレッドの適切な終了処理は、リソースリークや不定な状態を防ぐために重要です。
ここでは、スレッドが正しく終了することを保証する方法を紹介します。
#include <iostream>
#include <thread>
void task(int n) {
std::cout << "スレッド " << n << " からの出力" << std::endl;
}
int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);
t1.join();
t2.join();
std::cout << "メインスレッド終了" << std::endl;
return 0;
}
このコードでは、t1
とt2
の2つのスレッドを作成し、それぞれにtask
関数を割り当てています。
join()
メソッドを使用して、メインスレッドがこれらのスレッドの終了を待つことで、プログラムの正確な終了を保証しています。
●C++11の並列処理機能
C++11は、並列処理をサポートする多くの機能を導入しました。
これらの機能により、開発者はより効率的に並列処理を行えるようになりました。
C++11の並列処理機能には、スレッドの作成と管理、スレッド間の通信、データの同期などが含まれます。
これにより、プログラムの性能を最大限に引き出すことが可能になります。
○サンプルコード5:std::threadの活用
このコードは、std::threadを使用して新しいスレッドを作成し、実行する方法を表しています。
ここでは、std::threadを使って関数を並列に実行し、プログラムの効率を高める方法を紹介します。
この例では、std::threadを用いて関数を並行して実行し、プログラムの効率を向上させる手法を表しています。
#include <iostream>
#include <thread>
void function1() {
std::cout << "スレッド1が実行されています" << std::endl;
}
void function2() {
std::cout << "スレッド2が実行されています" << std::endl;
}
int main() {
std::thread thread1(function1);
std::thread thread2(function2);
thread1.join();
thread2.join();
return 0;
}
このコードは、function1とfunction2という2つの関数を用いて、2つのスレッドを生成して実行します。
それぞれのスレッドは独立して動作し、コンソールにメッセージを出力します。
thread1.join()とthread2.join()により、メインスレッドはこれらのスレッドの完了を待機します。
○サンプルコード6:std::asyncとstd::futureの使用
このコードは、std::asyncとstd::futureを用いて非同期処理を行う方法を表しています。
std::asyncは非同期タスクを開始し、std::futureはその結果を取得します。
この例では、非同期処理を行い、その結果を効率的に取得する方法を紹介します。
#include <iostream>
#include <future>
int calculate() {
return 10 * 2;
}
int main() {
std::future<int> result = std::async(calculate);
std::cout << "計算結果: " << result.get() << std::endl;
return 0;
}
このコードは、calculate関数を非同期に実行し、その結果をresult変数に保存します。
result.get()によって、非同期処理の結果を取得し、それをコンソールに出力します。
○サンプルコード7:ラムダ式を用いたスレッド処理
このコードは、ラムダ式を用いてスレッドを作成し、実行する方法を表しています。
ラムダ式は、匿名関数を簡潔に記述する手段を提供し、並列処理のコードをより読みやすくします。
この例では、ラムダ式を用いてスレッドを生成し、実行する方法を表しています。
#include <iostream>
#include <thread>
int main() {
std::thread thread1([]() {
std::cout << "ラムダ式によるスレッド1" << std::endl;
});
std::thread thread2([]() {
std::cout << "ラムダ式によるスレッド2" << std::endl;
});
thread1.join();
thread2.join();
return 0;
}
このコードは、ラムダ式を使用して2つのスレッドを生成します。
各スレッドは独立して動作し、コンソールにメッセージを出力します。
thread1.join()とthread2.join()により、メインスレッドはこれらのスレッドの完了を待機します。
●高度な並列処理技術
C++での高度な並列処理技術は、プログラミングの世界において重要な位置を占めています。
これらの技術は、複数の計算処理を同時に行うことで、アプリケーションのパフォーマンスを大幅に向上させることができます。
例えば、データベースのクエリ処理、画像処理、あるいは大規模な数値計算など、多くの分野で並列処理技術が活用されています。
並列処理技術の中核を成すのは、複数のスレッドまたはプロセスを同時に実行する能力です。
これにより、一つのプログラムが複数の作業を同時に進行させることが可能になります。
C++は、このような高度な処理をサポートするために、std::thread、std::asyncなどの強力なライブラリを提供しています。
○サンプルコード8:タスクベースの並列処理
タスクベースの並列処理は、特定のタスクを複数のスレッドに分散して処理する手法です。
この手法では、各スレッドが独立したタスクを実行し、全体の処理速度を向上させます。
#include <thread>
#include <vector>
void processTask(int taskID) {
// ここでタスクの処理を行う
}
int main() {
std::vector<std::thread> threads;
for(int i = 0; i < 10; ++i) {
threads.push_back(std::thread(processTask, i));
}
for(auto& thread : threads) {
thread.join();
}
return 0;
}
このコードは10個の異なるタスクを10個のスレッドで処理しています。
各スレッドはprocessTask
関数を実行し、異なるタスクIDを引数として受け取ります。
全てのスレッドがタスクを完了した後、join
メソッドを使用してメインスレッドに統合します。
○サンプルコード9:並列アルゴリズムの使用
C++には、標準ライブラリとして多数の並列アルゴリズムが含まれています。
これらのアルゴリズムを使用することで、データ処理や計算の効率を大幅に向上させることができます。
例えば、std::sort
関数を並列化することで、大量のデータを高速に整列させることが可能です。
#include <algorithm>
#include <vector>
#include <execution>
int main() {
std::vector<int> data = {9, 3, 5, 1, 7, 4, 6, 2, 8, 0};
std::sort(std::execution::par, data.begin(), data.end());
// dataは並列処理によってソートされる
return 0;
}
このコードでは、std::sort
関数にstd::execution::par
を指定することで、並列処理によるソートを実行しています。
これにより、大規模なデータセットでも高速にソート処理を行うことができます。
○サンプルコード10:マルチスレッドによるデータ処理
マルチスレッドによるデータ処理は、データセットを複数のスレッドに分割し、それぞれでデータ処理を行う方法です。
この手法は、大規模なデータ処理やリアルタイムシステムにおいて特に有効です。
#include <thread>
#include <vector>
void processDataSegment(int start, int end) {
// ここでデータセグメントの処理を行う
}
int main() {
int dataSize = 1000;
int numThreads = 10;
std::vector<std::thread> threads;
for(int i = 0; i < numThreads; ++i) {
int start = (dataSize / numThreads) * i;
int end = (dataSize / numThreads) * (i + 1);
threads.push_back(std::thread(processDataSegment, start, end));
}
for(auto& thread : threads) {
thread.join();
}
return 0;
}
このコードでは、データセットを10個のセグメントに分割し、各セグメントを異なるスレッドで処理しています。
これにより、データ処理の効率を大幅に向上させることができます。
●並列処理の応用例
並列処理は、様々なアプリケーションでその効果を発揮します。
特に、計算が複雑で時間がかかるタスクや、リアルタイムでの高速な処理が求められる状況では、並列処理の技術が不可欠です。
その応用例として、大規模な数値計算の最適化、リアルタイムデータ処理、そしてゲーム開発などが挙げられます。
○サンプルコード11:並列計算の最適化
数値計算の最適化において、並列処理は計算時間を劇的に短縮できます。
ここでは、大規模な数値計算を並列処理で高速化するサンプルコードを紹介します。
#include <vector>
#include <thread>
#include <functional>
void compute(std::vector<double>& data, int start, int end) {
for (int i = start; i < end; ++i) {
data[i] = /* 計算処理 */;
}
}
int main() {
const int dataSize = 10000;
std::vector<double> data(dataSize);
std::thread t1(compute, std::ref(data), 0, dataSize / 2);
std::thread t2(compute, std::ref(data), dataSize / 2, dataSize);
t1.join();
t2.join();
return 0;
}
このコードでは、データセットを2つのスレッドに分割し、それぞれで数値計算を行っています。
これにより、計算処理の時間を効果的に短縮できます。
○サンプルコード12:リアルタイムデータ処理
リアルタイムデータ処理では、データの流れが途切れることなく、迅速に処理されることが要求されます。
ここでは、リアルタイムデータ処理を実現するためのサンプルコードを紹介します。
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
void producer() {
for (int i = 0; i < 100; ++i) {
std::unique_lock<std::mutex> lock(mtx);
dataQueue.push(i);
cv.notify_one();
}
finished = true;
cv.notify_one();
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !dataQueue.empty() || finished; });
if (finished && dataQueue.empty()) {
break;
}
int data = dataQueue.front();
dataQueue.pop();
lock.unlock();
// データ処理
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
このコードでは、プロデューサーがデータを生成し、コンシューマーがそれを処理します。
これにより、リアルタイムでデータが流れる状況をシミュレートし、効果的に処理を行います。
○サンプルコード13:並列処理を活用したゲーム開発
ゲーム開発においても、並列処理は重要な役割を果たします。
特にグラフィックス処理や物理シミュレーションでは、複数のスレッドを使って処理を行うことが一般的です。
#include <thread>
#include <vector>
void updateGraphics() {
// グラフィックスの更新処理
}
void updatePhysics() {
// 物理エンジンの更新処理
}
int main() {
std::thread graphicsThread(updateGraphics);
std::thread physicsThread(updatePhysics);
graphicsThread.join();
physicsThread.join();
return 0;
}
このコードでは、グラフィックスと物理エンジンの処理を別々のスレッドで実行しています。
これにより、ゲームのレンダリングと物理計算を同時に進行させ、全体のパフォーマンスを向上させます。
●並列処理の注意点と対処法
C++での並列処理を行う際、複数のスレッドが同時に動作することで、特有の問題が発生することがあります。
これらの問題を理解し、適切に対処することは、効率的で安全な並列処理プログラムを実現するために不可欠です。
並列処理を行う上で最も一般的な問題は、デッドロック、レースコンディション、そしてパフォーマンスの問題です。
これらの問題にはそれぞれ特有の原因があり、それに応じた対処法が必要となります。ここでは、これらの問題とその対処法について詳しく解説します。
○デッドロックとその回避方法
デッドロックは、複数のスレッドがお互いのリソースを待ち続けることで、進行不能な状態に陥る現象です。
この問題を回避するためには、リソースの取得順序を一定に保つ、リソースの取得にタイムアウトを設定する、リソースの取得を試みる前に必要なすべてのリソースが利用可能であることを確認する、などの方法があります。
例えば、下記のコードはスレッドが異なる順序でロックを取得しようとすることによってデッドロックを引き起こす可能性があります。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread1() {
std::lock_guard<std::mutex> lock1(mutex1);
// 何らかの処理
std::lock_guard<std::mutex> lock2(mutex2);
// 何らかの処理
}
void thread2() {
std::lock_guard<std::mutex> lock2(mutex2);
// 何らかの処理
std::lock_guard<std::mutex> lock1(mutex1);
// 何らかの処理
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
このコードは、スレッド1とスレッド2が異なる順序でmutex1とmutex2をロックしようとするため、デッドロックのリスクがあります。
これを回避するためには、両方のスレッドがリソースを同じ順序で取得するように変更することが有効です。
○レースコンディションとその対策
レースコンディションは、複数のスレッドが同時に共有データにアクセスしようとしたときに発生する問題です。
これを防ぐためには、データアクセス時に適切なロック(mutexなど)を使用することが重要です。
例えば、下記のコードでは複数のスレッドが同時に共有データshared_data
にアクセスしていますが、適切な同期が行われていないため、レースコンディションが発生する可能性があります。
#include <iostream>
#include <thread>
#include <vector>
int shared_data = 0;
void increment() {
for (int i = 0; i < 10000; ++i) {
++shared_data; // レースコンディションの可能性
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(increment));
}
for (auto& t : threads) {
t.join();
}
std::cout << "共有データの値: " << shared_data << std::endl;
return 0;
}
この問題を解決するには、shared_data
へのアクセスをmutexで保護することが有効です。
これにより、一度に一つのスレッドだけがデータにアクセスできるようになり、レースコンディションを防ぐことができます。
○パフォーマンスの最適化
並列処理のパフォーマンスを最適化するためには、スレッドの過剰な生成を避け、適切な数のスレッドを使用することが重要です。
また、スレッド間の通信や同期にはコストがかかるため、これらを最小限に抑えることもパフォーマンス向上に寄与します。
例えば、下記のコードではスレッドを大量に生成していますが、これはオーバーヘッドを増加させ、パフォーマンスを低下させる可能性があります。
#include <thread>
#include <vector>
void task() {
// 何らかの処理
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 1000; ++i) {
threads.push_back(std::thread(task));
}
for (auto& t : threads) {
t.join();
}
return 0;
}
このような場合、スレッドプールを使用するか、タスクを適切な数のスレッドに分割して処理することで、パフォーマンスを改善することができます。
●C++での並列処理のカスタマイズ
C++の並列処理をカスタマイズすることは、特定のアプリケーションに最適なパフォーマンスを実現するために非常に重要です。
標準のスレッドや同期メカニズムを超えて、より高度な制御を実現するために、カスタムスレッドプールの作成や条件変数を用いた同期などの技術があります。
これらの技術を用いることで、特定の要件に合わせた効率的な並列処理が可能となります。
○サンプルコード14:カスタムスレッドプールの作成
スレッドプールは、スレッドの生成と破棄のオーバーヘッドを減らすために使われます。
スレッドプール内では、既に生成されたスレッドがタスクを待機し、タスクが利用可能になると即座に処理を開始します。
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
public:
ThreadPool(size_t threads) : stop(false) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
};
このコードは、指定された数のワーカースレッドを持つスレッドプールを作成し、タスクをキューに追加して順次実行します。
○サンプルコード15:条件変数を用いた高度な同期
条件変数は、特定の条件が満たされるのを待つために使用されます。
これにより、必要な時にのみスレッドが起動され、無駄なリソース消費を避けることができます。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
std::cout << "スレッド " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10スレッドが待機しています...\n";
go();
for (auto& th : threads) th.join();
return 0;
}
このコードでは、10個のスレッドがready
がtrue
になるのを待っています。
go
関数が呼ばれると、ready
がtrue
に設定され、すべてのスレッドが起動します。
まとめ
C++における並列処理の学習と応用は、プログラミングスキルを大きく向上させるための重要なステップです。
この記事では基本から応用まで、さまざまな並列処理技術を実例と共に解説しました。
スレッドの基本操作から、マルチスレッディング、カスタムスレッドプールの作成、条件変数を使用した高度な同期まで、C++での並列処理を幅広くカバーしました。
これらの知識とサンプルコードを活用して、あなたのC++プログラミングを次のレベルへと進めてください。