C++標準ライブラリは、文字列処理、コンテナ、アルゴリズム、入出力、メモリ管理、ファイル操作、並行処理など、C++プログラムで必要となる多くの機能を提供しています。
基本的な説明としては、コンテナ、イテレータ、アルゴリズムの関係を理解することが重要です。
ただし、実務で正しく使用するためには、処理速度、オブジェクトの寿命、イテレータの無効化、C++規格ごとの機能差なども理解しておく必要があります。
特に注意が必要なのは、次のような点です。
std::listの挿入や削除が高速になる条件std::unordered_mapの平均計算量と最悪計算量std::jthreadの破棄時の動作std::string_viewやstd::spanの参照先の寿命std::random_deviceと乱数の安全性- C++11、C++17、C++20などによる機能差
C++標準ライブラリとSTLの違い
C++標準ライブラリとは
C++標準ライブラリは、C++の規格で定められているライブラリ全体を指します。
主な機能には、次のものがあります。
- 文字列処理
- コンテナ
- イテレータ
- アルゴリズム
- 標準入出力
- ファイル入出力
- 数値計算
- メモリ管理
- 乱数生成
- 正規表現
- 日付・時刻
- ファイルシステム
- マルチスレッド
- 型情報
- Range
- Concept
C++標準ライブラリの多くは、std名前空間に定義されています。
std::string text = "Hello";
std::vector<int> values = {1, 2, 3};
std::cout << text << '\n';
STLとは
STLは、Standard Template Libraryの略称です。
一般的には、次の仕組みを中心とした設計を指します。
- コンテナ
- イテレータ
- アルゴリズム
- 関数オブジェクト
- アロケータ
例えば、std::vectorに格納された要素を、イテレータ経由でstd::sortに渡す構造は、STLを代表する設計です。
std::vector<int> values = {3, 1, 2};
std::sort(values.begin(), values.end());
C++標準ライブラリとSTLは完全に同じ意味ではありません。
std::vectorやstd::sortはSTLの中心的な機能ですが、std::thread、std::filesystem、std::iostreamなどを含む全体は、C++標準ライブラリと呼ぶほうが正確です。
std::vectorの特徴と注意点
std::vectorは連続したメモリを使用する
std::vectorは、可変長の動的配列です。
std::vector<int> values = {10, 20, 30};
要素は原則として連続したメモリ領域に格納されるため、添字によるアクセスを高速に行えます。
std::cout << values[0] << '\n';
主な特徴は次のとおりです。
- 添字アクセスが高速
- 末尾への追加が効率的
- 要素が連続して配置される
- キャッシュ効率が高い
- 途中への挿入や削除では要素移動が発生する
一般的な可変長コンテナが必要な場合は、最初にstd::vectorを検討するのが基本です。
末尾追加は償却定数時間
push_back()やemplace_back()による末尾追加は、平均的には効率的です。
values.push_back(40);
values.emplace_back(50);
ただし、常に一定時間で完了するわけではありません。
容量が不足すると、新しいメモリ領域を確保し、既存の要素を移動またはコピーする再確保が発生します。
そのため、末尾追加の計算量は、一般的に償却定数時間と表現されます。
reserveとresizeの違い
追加する要素数があらかじめ分かっている場合は、reserve()で容量を確保できます。
std::vector<int> values;
values.reserve(100);
ただし、reserve()は要素数を増やしません。
std::cout << values.size(); // 0
そのため、次のコードは不正です。
values[0] = 10;
要素を実際に作成する場合は、push_back()やresize()を使用します。
values.push_back(10);
または、次のようにします。
values.resize(100);
values[0] = 10;
再確保による参照の無効化
std::vectorで再確保が発生すると、既存要素を指していた次のものが無効になる可能性があります。
- イテレータ
- ポインタ
- 参照
- 過去終端イテレータ
次のようなコードには注意が必要です。
std::vector<int> values = {1, 2, 3};
int& first = values[0];
values.push_back(4);
std::cout << first;
push_back()で再確保が発生した場合、firstは無効な参照になります。
operator[]とat()の違い
operator[]は範囲検査を行わない
std::vectorのoperator[]は、通常、範囲外アクセスを検査しません。
std::vector<int> values = {1, 2, 3};
int value = values[100];
このような範囲外アクセスは、未定義動作につながります。
at()は範囲外で例外を送出する
at()は範囲を検査し、範囲外の場合はstd::out_of_rangeを送出します。
try {
int value = values.at(100);
} catch (const std::out_of_range& error) {
std::cerr << error.what() << '\n';
}
安全性を重視する場合はat()が適しています。
一方、添字が必ず正しいことが保証されており、範囲検査の負担を避けたい場合はoperator[]が使用されます。
std::listの特徴と注意点
挿入や削除が高速になる条件
std::listは双方向連結リストです。
std::list<int> values = {10, 20, 30};
挿入位置や削除位置を示すイテレータをすでに持っている場合、その位置での挿入や削除を効率的に行えます。
auto it = values.begin();
++it;
values.insert(it, 15);
ただし、対象位置を探す処理まで高速になるわけではありません。
auto it = std::find(
values.begin(),
values.end(),
target
);
if (it != values.end()) {
values.erase(it);
}
この場合、対象要素の検索には先頭から順番に確認する処理が必要です。
std::listが常にvectorより速いわけではない
std::listは各要素を個別のノードとして管理します。
そのため、次のような負担があります。
- ノードごとのメモリ確保
- 前後ポインタ分のメモリ消費
- キャッシュ効率の低下
- ランダムアクセスができない
途中への挿入や削除があるという理由だけでstd::listを選ぶのは適切ではありません。
要素数、アクセス方法、検索方法、メモリ効率などを考慮したうえで選択する必要があります。
std::dequeの特徴
先頭と末尾の操作に適している
std::dequeは、先頭と末尾の両方へ要素を追加・削除しやすいコンテナです。
std::deque<int> values;
values.push_front(10);
values.push_back(20);
主な特徴は次のとおりです。
- 先頭への追加が効率的
- 末尾への追加も効率的
- ランダムアクセスが可能
- 全要素が一つの連続領域にあるとは限らない
連続メモリが必要な場合には向かない
std::dequeは、std::vectorのように全要素が一続きのメモリに配置されることを保証しません。
そのため、C言語のAPIなどへ連続配列として渡したい場合には、std::vectorのほうが適しています。
std::mapの特徴
キーと値を順序付きで管理する
std::mapは、キーと値の組を管理する連想コンテナです。
std::map<std::string, int> scores;
scores["Alice"] = 90;
scores["Bob"] = 80;
キーは比較関数に基づいて順序付けされます。
operator[]は要素を追加する
存在しないキーにoperator[]でアクセスすると、新しい要素が挿入されます。
int score = scores["Carol"];
intの場合、値は通常0で初期化されます。
検索だけを行いたい場合は、find()、contains()、at()などを使用します。
auto it = scores.find("Alice");
if (it != scores.end()) {
std::cout << it->second;
}
C++20以降では、存在確認だけならcontains()を使用できます。
if (scores.contains("Alice")) {
std::cout << "存在します\n";
}
存在しない場合に例外として扱いたい場合はat()を使用します。
try {
std::cout << scores.at("Alice");
} catch (const std::out_of_range&) {
std::cerr << "キーが存在しません\n";
}
std::unordered_mapの特徴
平均的には高速に検索できる
std::unordered_mapは、ハッシュテーブルを使用する連想コンテナです。
std::unordered_map<std::string, int> scores;
scores["Alice"] = 90;
scores["Bob"] = 80;
検索、挿入、削除は、平均的には定数時間で行えます。
平均 O(1)
ただし、ハッシュ衝突が多い場合などは、最悪で線形時間になる可能性があります。
最悪 O(n)
要素はキー順に並ばない
std::unordered_mapでは、要素の反復順序はキー順になりません。
キーを並べた状態で処理したい場合は、std::mapのほうが適しています。
リハッシュに注意する
要素数が増えると、内部のバケット数を変更するリハッシュが発生することがあります。
リハッシュが発生すると、既存のイテレータは無効になります。
ただし、リハッシュだけで要素そのものが削除されるわけではないため、要素への参照やポインタは通常維持されます。
std::setの特徴
重複しない値を順序付きで管理する
std::setは、重複しない値を比較関数に基づいて管理します。
std::set<int> values = {3, 1, 2, 2};
格納される要素は、次のようになります。
1, 2, 3
入力順は維持されない
std::setは値を順序付けするため、入力順を維持したまま重複だけを除去する用途には向きません。
入力順を維持したい場合は、例えばstd::unordered_setで既出確認を行い、結果をstd::vectorに格納します。
std::vector<int> input = {3, 1, 2, 2, 1};
std::unordered_set<int> seen;
std::vector<int> result;
for (int value : input) {
if (seen.insert(value).second) {
result.push_back(value);
}
}
結果は次のようになります。
3, 1, 2
イテレータの基本
イテレータは要素を指し示す仕組み
イテレータは、コンテナ内の要素を参照するためのオブジェクトです。
std::vector<int> values = {10, 20, 30};
auto it = values.begin();
std::cout << *it;
ポインタに似た操作ができますが、すべてのイテレータが生ポインタであるわけではありません。
end()は最後の要素ではない
end()は、最後の要素の次の位置を表します。
auto it = values.end();
end()が返すイテレータをデリファレンスしてはいけません。
std::cout << *it;
この操作は不正です。
コンテナごとに利用できる操作が異なる
std::vectorのイテレータはランダムアクセスできます。
auto it = values.begin();
it += 2;
一方、std::listのイテレータでは、同じ操作はできません。
std::list<int> values = {1, 2, 3};
auto it = values.begin();
// it += 2; // 使用不可
移動にはstd::advance()を使用できます。
std::advance(it, 2);
ただし、std::listでは移動距離に応じた時間がかかります。
標準アルゴリズムの使い方
std::sortによる並べ替え
std::sortは、ランダムアクセスイテレータを持つ範囲を並べ替えます。
std::vector<int> values = {3, 1, 2};
std::sort(values.begin(), values.end());
降順に並べる場合は、比較関数を指定します。
std::sort(
values.begin(),
values.end(),
std::greater<int>{}
);
std::listには専用のsortを使用する
std::listのイテレータはランダムアクセスイテレータではないため、std::sortは使用できません。
std::list<int> values = {3, 1, 2};
values.sort();
Rangeライブラリ
std::ranges::sort
C++20以降では、Range版のアルゴリズムを利用できます。
std::ranges::sort(values);
従来のようにbegin()とend()を明示する必要がありません。
射影を使った並べ替え
構造体の特定メンバを基準に並べ替える場合、射影を利用できます。
struct Product {
std::string name;
int price;
};
std::vector<Product> products = {
{"Brush A", 1200},
{"Brush B", 800}
};
std::ranges::sort(
products,
std::ranges::less{},
&Product::price
);
このコードでは、Product::priceを基準に昇順で並べ替えます。
すべてのRangeをソートできるわけではない
std::ranges::sortを使用するには、対象がランダムアクセス可能で、要素の並べ替えが可能である必要があります。
そのため、std::listには使用できません。
std::list<int> values = {3, 1, 2};
// std::ranges::sort(values); // 使用不可
std::removeと要素削除
std::removeはコンテナサイズを変更しない
std::removeは、削除対象以外の要素を前方へ移動し、新しい論理的終端を返します。
std::vector<int> values = {1, 2, 3, 2, 4};
auto new_end = std::remove(
values.begin(),
values.end(),
2
);
この時点では、values.size()は変わりません。
eraseと組み合わせて削除する
実際に要素数を減らすには、erase()と組み合わせます。
values.erase(
std::remove(values.begin(), values.end(), 2),
values.end()
);
C++20以降では、次のように簡潔に書けます。
std::erase(values, 2);
条件付き削除にはstd::erase_if()を使用します。
std::erase_if(
values,
[](int value) {
return value % 2 == 0;
}
);
std::accumulateの注意点
初期値の型が結果型に影響する
std::accumulateでは、初期値の型が累積処理の型に影響します。
std::vector<double> values = {1.5, 2.5};
double total = std::accumulate(
values.begin(),
values.end(),
0.0
);
初期値を0にすると、整数型で累積され、小数部分が失われる可能性があります。
double total = std::accumulate(
values.begin(),
values.end(),
0
);
整数の大きな合計を求める場合は、初期値を0LLにするなど、結果型を明示します。
long long total = std::accumulate(
values.begin(),
values.end(),
0LL
);
std::string_viewの注意点
文字列を所有しない
std::string_viewは、既存の文字列データを参照する非所有型です。
void print_text(std::string_view text) {
std::cout << text;
}
文字列をコピーせずに受け取れるため、読み取り専用の関数引数などに適しています。
参照先の寿命に注意する
次のコードは危険です。
std::string_view get_text() {
std::string text = "Hello";
return text;
}
関数終了時にtextが破棄されるため、返されたstring_viewは有効な文字列を参照していません。
文字列リテラルを返す場合は安全です。
std::string_view get_text() {
return "Hello";
}
所有権が必要な場合は、std::stringを返します。
std::string get_text() {
return "Hello";
}
参照元の再確保でも無効になる
参照元のstd::stringが生存していても、内部バッファの再確保によってstring_viewが無効になる場合があります。
std::string text = "Hello";
std::string_view view = text;
text += " World";
std::cout << view;
text += " World"によって再確保が発生すると、viewは古い領域を参照する可能性があります。
std::spanの注意点
連続した要素列を参照する
std::spanは、配列やstd::vectorなどの連続した要素列を参照する非所有型です。
void print_values(std::span<const int> values) {
for (int value : values) {
std::cout << value << '\n';
}
}
次のような異なるコンテナを同じ関数へ渡せます。
std::vector<int> vector_values = {1, 2, 3};
std::array<int, 3> array_values = {4, 5, 6};
print_values(vector_values);
print_values(array_values);
参照先の寿命を延ばさない
std::spanもデータを所有しません。
std::span<const int> stored_view;
void set_values() {
std::vector<int> values = {1, 2, 3};
stored_view = values;
}
関数終了後、valuesは破棄されるため、stored_viewは無効になります。
参照元のstd::vectorで再確保が発生した場合も、既存のspanが無効になる可能性があります。
スマートポインタ
std::unique_ptr
std::unique_ptrは、単独所有を表すスマートポインタです。
auto pointer = std::make_unique<int>(100);
コピーはできません。
// auto another = pointer; // エラー
所有権は移動できます。
auto another = std::move(pointer);
単独所有で十分な場合は、std::shared_ptrよりもstd::unique_ptrを優先するのが基本です。
std::shared_ptr
std::shared_ptrは、複数の所有者でオブジェクトを共有します。
auto pointer1 = std::make_shared<int>(100);
auto pointer2 = pointer1;
強い所有数が0になると、管理対象オブジェクトが破棄されます。
ただし、std::weak_ptrが残っている場合、管理情報を持つ制御ブロックは残ることがあります。
std::weak_ptr
std::weak_ptrは、std::shared_ptrの管理対象を所有せずに参照します。
std::weak_ptr<int> weak = pointer1;
使用する場合は、lock()で有効なshared_ptrを取得します。
if (auto shared = weak.lock()) {
std::cout << *shared;
}
std::shared_ptr同士の循環参照を防ぐ用途にも使用されます。
std::optional
値の有無を表現する
std::optionalは、値が存在する状態と存在しない状態を表現します。
std::optional<int> find_value(bool found) {
if (found) {
return 100;
}
return std::nullopt;
}
値の存在確認は次のように行います。
auto result = find_value(true);
if (result) {
std::cout << *result;
}
値がない状態でoperator*を使用しない
値を持っていないoptionalに対してoperator*を使用してはいけません。
例外として扱いたい場合はvalue()を使用します。
try {
std::cout << result.value();
} catch (const std::bad_optional_access&) {
std::cerr << "値がありません\n";
}
デフォルト値を使用する場合はvalue_or()が便利です。
int value = result.value_or(0);
std::variant
複数の候補型から一つを保持する
std::variantは、指定された複数の型のうち、いずれか一つの値を保持します。
std::variant<int, std::string> value;
value = 100;
value = std::string("Hello");
保持している型を確認できます。
if (std::holds_alternative<std::string>(value)) {
std::cout << std::get<std::string>(value);
}
std::visitで型ごとの処理を行う
std::visit(
[](const auto& item) {
std::cout << item;
},
value
);
このコードが成立するには、variantに含まれるすべての型が出力可能である必要があります。
型ごとに処理を分ける場合は、複数のラムダを組み合わせて使用します。
std::any
任意の型を格納する
std::anyは、任意の型の値を格納できます。
std::any value = 100;
value = std::string("Hello");
値を取り出すにはstd::any_castを使用します。
try {
auto text = std::any_cast<std::string>(value);
} catch (const std::bad_any_cast&) {
std::cerr << "型が一致しません\n";
}
コピーを避けたい場合は参照として取得できます。
const auto& text =
std::any_cast<const std::string&>(value);
例外を使用せずに確認する場合は、ポインタ版を使用します。
if (const auto* text =
std::any_cast<std::string>(&value)) {
std::cout << *text;
}
std::move
std::move自体はデータを移動しない
std::moveは、オブジェクトを右辺値として扱えるように変換する機能です。
std::string text = "Hello";
std::string another = std::move(text);
実際の移動処理を行うのは、代入先の型が持つムーブコンストラクタやムーブ代入演算子です。
移動後のオブジェクトは有効ですが、一般的には元の内容が維持されていると仮定してはいけません。
text.clear();
text = "New text";
破棄や再代入は可能です。
std::thread
joinまたはdetachが必要
std::threadでスレッドを作成した場合、オブジェクトが破棄される前にjoin()またはdetach()を実行する必要があります。
std::thread worker(task);
worker.join();
join可能な状態のstd::threadが破棄されると、std::terminate()が呼び出されます。
detachは慎重に使用する
worker.detach();
detach()を使用すると、スレッドは独立して実行を続けます。
ただし、スレッドが参照しているデータの寿命を管理しにくくなるため、安易に使用するべきではありません。
std::jthread
破棄時に停止要求とjoinを行う
C++20以降では、std::jthreadを使用できます。
std::jthread worker(task);
join可能なstd::jthreadが破棄されると、停止要求を出したうえでjoinします。
std::stop_tokenと組み合わせると、停止要求を確認できます。
std::jthread worker([](std::stop_token token) {
while (!token.stop_requested()) {
// 処理
}
});
ただし、停止要求は強制終了ではありません。
処理側がstop_requested()などを確認しなければ、停止要求に応じない場合があります。
std::condition_variable
状態変化をスレッドへ通知する
条件変数は、特定の条件が満たされるまでスレッドを待機させるために使用します。
std::unique_lock<std::mutex> lock(mutex);
condition.wait(lock, [&ready] {
return ready;
});
述語付きのwait()を使用することで、意図しない起床にも対応できます。
通知側では、共有状態をロック内で変更してから通知します。
{
std::lock_guard<std::mutex> lock(mutex);
ready = true;
}
condition.notify_one();
std::async
実行ポリシーを明示する
std::asyncを利用すると、関数の実行結果をstd::futureで受け取れます。
auto future = std::async(
std::launch::async,
calculate
);
int result = future.get();
std::launch::asyncを指定すると、非同期実行が要求されます。
実行ポリシーを省略した場合、実装によって非同期実行または遅延実行が選択される可能性があります。
auto future = std::async(calculate);
そのため、必ず非同期実行させたい場合はstd::launch::asyncを明示します。
std::atomic
単一操作をスレッドセーフにする
std::atomicは、特定の操作をアトミックに実行するために使用します。
std::atomic<int> counter = 0;
void increment() {
++counter;
}
複数スレッドからインクリメントしても、この操作自体によるデータ競合は発生しません。
ただし、複数の操作を組み合わせた業務上の整合性まで自動的に保証されるわけではありません。
複雑な条件判定と更新をまとめて保護する場合は、ミューテックスなどが必要になることがあります。
乱数ライブラリ
mt19937による乱数生成
一般的な乱数生成では、乱数エンジンと分布を組み合わせます。
std::random_device seed;
std::mt19937 generator(seed());
std::uniform_int_distribution<int> distribution(1, 6);
int dice = distribution(generator);
std::uniform_int_distribution<int>(1, 6)は、1から6までを含む整数を生成します。
random_deviceは実装によって性質が異なる
std::random_deviceは、非決定的な乱数源を提供することを意図しています。
ただし、実際の性質は実装環境によって異なり、必ずハードウェア由来の真の乱数を返すとは限りません。
また、std::mt19937は暗号学的に安全な乱数生成器ではありません。
暗号鍵、認証トークン、パスワード関連などの用途では、OSや暗号ライブラリが提供する安全な乱数生成機能を使用する必要があります。
std::regex
regex_matchとregex_searchの違い
std::regex_matchは、文字列全体がパターンに一致するかを確認します。
std::string text = "abc123";
std::regex pattern(R"([a-z]+[0-9]+)");
if (std::regex_match(text, pattern)) {
std::cout << "一致しました\n";
}
std::regex_searchは、文字列の一部分に一致箇所があるかを確認します。
if (std::regex_search(text, pattern)) {
std::cout << "一部に一致しました\n";
}
単純な固定文字列検索であれば、std::string::find()のほうが軽量で分かりやすい場合があります。
std::filesystem
ファイルやディレクトリを操作する
C++17以降では、std::filesystemを使用できます。
namespace fs = std::filesystem;
if (fs::exists("data.txt")) {
std::cout << "存在します\n";
}
ディレクトリ作成も可能です。
fs::create_directory("output");
複数階層をまとめて作成する場合は、create_directories()を使用します。
fs::create_directories("output/images");
例外とerror_code
ファイル操作は、権限不足やパス不正などによって失敗する可能性があります。
例外を使用しない場合は、std::error_codeを渡せます。
std::error_code error;
bool created =
fs::create_directory(
"output",
error
);
if (error) {
std::cerr << error.message() << '\n';
}
removeとremove_allの違い
remove()は、一つのファイルまたは空のディレクトリを削除します。
fs::remove("data.txt");
ディレクトリ配下を再帰的に削除する場合はremove_all()を使用します。
fs::remove_all("output");
remove_all()は大量のファイルを削除する可能性があるため、対象パスを十分に確認する必要があります。
std::format
C++20以降の文字列書式化
std::formatを使用すると、値を指定した形式で文字列へ埋め込めます。
std::string result = std::format(
"{} is {} years old.",
name,
age
);
小数点以下の桁数も指定できます。
std::string result =
std::format("{:.2f}", price);
利用するには、C++20以降の言語モードと、対応した標準ライブラリ実装が必要です。
古いコンパイラや標準ライブラリでは、C++20モードでも<format>が十分に実装されていない場合があります。
std::chrono
処理時間の計測にはsteady_clockを使う
処理時間を測定する場合は、std::chrono::steady_clockが適しています。
auto start = std::chrono::steady_clock::now();
// 計測対象の処理
auto end = std::chrono::steady_clock::now();
steady_clockは単調増加する時計として使用でき、システム時刻の調整による影響を受けにくい特徴があります。
現在日時にはsystem_clockを使う
現実世界の日時を取得する場合は、std::chrono::system_clockを使用します。
用途によって使い分ける必要があります。
- 経過時間の測定:
steady_clock - 現在日時の取得:
system_clock
high_resolution_clockは、実装によって別の時計の別名になっている場合があります。
処理時間の測定では、目的が明確なsteady_clockを使用するほうが安全です。
浮動小数点数の比較
単純な等価比較には注意する
浮動小数点数には丸め誤差があるため、単純な==比較が期待どおりにならない場合があります。
double a = 0.1 + 0.2;
double b = 0.3;
簡単な例では、許容誤差を使って比較します。
if (std::abs(a - b) < 1e-9) {
std::cout << "ほぼ等しい\n";
}
ただし、固定された絶対誤差だけでは、値が非常に大きい場合や小さい場合に適切でないことがあります。
より一般的には、絶対誤差と相対誤差を組み合わせます。
bool nearly_equal(
double a,
double b,
double relative_tolerance = 1e-9,
double absolute_tolerance = 1e-12
) {
const double difference = std::abs(a - b);
return difference <= std::max(
absolute_tolerance,
relative_tolerance *
std::max(std::abs(a), std::abs(b))
);
}
適切な許容誤差は、金額計算、物理計算、幾何学計算などの用途によって異なります。
std::priority_queue
デフォルトでは最大値を取り出す
std::priority_queueは、デフォルトでは最大値をtop()で返します。
std::priority_queue<int> values;
values.push(10);
values.push(30);
values.push(20);
std::cout << values.top(); // 30
最小値を優先する
最小値をtop()で取得したい場合は、std::greaterを指定します。
std::priority_queue<
int,
std::vector<int>,
std::greater<int>
> values;
全要素を優先度順に処理するには、top()とpop()を繰り返します。
while (!values.empty()) {
std::cout << values.top() << '\n';
values.pop();
}
std::forward
完全転送に使用する
std::forwardは、テンプレート関数で引数の値カテゴリを維持したまま別の関数へ渡すために使用します。
template <typename T>
void wrapper(T&& value) {
function(std::forward<T>(value));
}
この形のT&&は、関数呼び出し時にTが推論される場合、転送参照として機能します。
ただし、すべてのT&&が転送参照になるわけではありません。
テンプレートパラメータがすでに決定している場合などは、通常の右辺値参照になります。
C++規格ごとの主な機能
C++11
C++11では、現代的なC++の基礎となる多くの機能が導入されました。
主な機能は次のとおりです。
std::unique_ptrstd::shared_ptrstd::weak_ptrstd::threadstd::mutexstd::condition_variablestd::futurestd::asyncstd::atomicstd::arraystd::unordered_mapstd::unordered_setstd::tuple- ラムダ式
- ムーブセマンティクス
std::chrono<random><regex>
C++14
C++14では、C++11の機能が改善されました。
代表的な機能には、次のものがあります。
std::make_unique- ジェネリックラムダ
- 戻り値型推論の改善
std::unique_ptrはC++11ですが、std::make_uniqueはC++14で導入されました。
C++17
C++17では、次のような機能が追加されました。
std::optionalstd::variantstd::anystd::string_viewstd::filesystem- 構造化束縛
_v形式の型特性
C++20
C++20では、標準ライブラリとテンプレート機能が大きく拡張されました。
主な機能は次のとおりです。
- Concepts
- Ranges
std::spanstd::formatstd::jthreadstd::stop_tokencontains()std::erase()std::erase_if()<bit>の主要機能
コードを使用する際は、対象機能がどのC++規格で導入されたかを確認し、コンパイラの言語モードを適切に設定する必要があります。
必要なヘッダを明示する
使用する機能に対応したヘッダを読み込む
標準ライブラリの機能を使用する場合は、対応するヘッダを明示的に読み込みます。
例えば、std::spanを使用する場合は次が必要です。
#include <span>
std::coutを使用する場合は次が必要です。
#include <iostream>
std::greaterを使用する場合は、次を読み込むのが適切です。
#include <functional>
一部の環境では、別のヘッダを読み込んだ結果として必要な機能が偶然利用できる場合があります。
ただし、そのような間接的なインクルードに依存せず、自分が直接使用する機能のヘッダを明示することが重要です。
実践的な商品データの処理例
価格順に並べて販売可能な商品だけを表示する
次のコードは、商品を価格順に並べ替え、販売可能な商品だけを表示する例です。
#include <algorithm>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>
struct Product {
std::string name;
int price;
bool available;
};
int main() {
std::vector<Product> products = {
{"Brush A", 1200, true},
{"Brush B", 800, false},
{"Brush C", 1500, true},
{"Brush D", 1000, true}
};
std::ranges::sort(
products,
std::ranges::less{},
&Product::price
);
auto available_products =
products
| std::views::filter(
[](const Product& product) {
return product.available;
}
);
for (const Product& product : available_products) {
std::cout
<< product.name
<< ": "
<< product.price
<< "円\n";
}
}
出力結果は次のとおりです。
Brush D: 1000円
Brush A: 1200円
Brush C: 1500円
Brush Bは価格が最も低い商品ですが、availableがfalseであるため表示されません。
このコードでは、次の機能を使用しています。
std::vectorstd::stringstd::ranges::sort- 射影
std::views::filter- ラムダ式
- 範囲ベースfor文
C++標準ライブラリを使用する際の重要ポイント
コンテナは用途に応じて選ぶ
一般的な可変長配列には、まずstd::vectorを検討します。
先頭と末尾を頻繁に操作する場合はstd::deque、キー順で管理する場合はstd::map、高速なキー検索が中心の場合はstd::unordered_mapが候補になります。
非所有型では寿命を確認する
std::string_viewやstd::spanは、コピーを避けられる便利な型です。
一方で、参照先の寿命を管理しないため、参照先の破棄や再確保に注意する必要があります。
平均計算量だけで判断しない
std::unordered_mapは平均的には高速ですが、最悪計算量やリハッシュの影響もあります。
std::listの挿入や削除も、対象位置をすでに把握している場合に効率的です。
計算量の説明では、前提条件まで確認する必要があります。
C++規格と実装状況を確認する
Range、Concept、std::span、std::format、std::jthreadなどはC++20以降の機能です。
コンパイラが対応する言語モードだけでなく、使用している標準ライブラリの実装状況も確認する必要があります。
所有権を明確にする
単独所有にはstd::unique_ptrを優先し、共有所有が本当に必要な場合にstd::shared_ptrを使用します。
所有しない参照としてポインタや参照を使う場合は、参照先の寿命を明確にすることが重要です。
まとめ
C++標準ライブラリには、データ管理、検索、並べ替え、文字列処理、メモリ管理、ファイル操作、並行処理など、多くの機能が用意されています。
各機能の基本的な使い方だけでなく、次の点まで理解すると、より安全で効率的なコードを書けます。
- コンテナごとの内部構造と計算量
- イテレータや参照が無効になる条件
- 所有する型と所有しない型の違い
- スレッドや非同期処理の終了管理
- C++規格ごとの対応状況
- 標準ライブラリ実装ごとの差
標準ライブラリを正しく活用するうえでは、機能名を暗記することよりも、データの所有権、オブジェクトの寿命、処理速度、無効化条件を理解することが重要です。
以上、C++の標準ライブラリについてでした。
最後までお読みいただき、ありがとうございました。
