546.3K Views
August 21, 24
スライド概要
【🌟 より詳しい解説・サンプルを収録した書籍 / 電子版】https://techbookfest.org/product/1p61X6cAyA7aVThMuM3sj6
【🎮 CEDEC ページ】https://cedec.cesa.or.jp/2024/session/detail/s6609118bb9dfd/
【⏲ 前回 2020 のスライド】https://speakerdeck.com/cpp/cedec2020
C++
ゲーム開発者のための C++17 ~ C++23, 近年の C++ 規格策定の動向 鈴木 遼 松村 哲郎 安藤 弘晃 cpprefjp / Siv3D cpprefjp cpprefjp
C++ に強くなる 60 分! ◆ 前半: ゲーム開発に役立つ C++17 ~ C++23 の機能 • C++17 / C++20 / C++23 でより使いやすくなった C++, ゲームやツール開発に役立つ機能をピックアップして 35 個のガイドラインに。 • 講演を聞くことで、モダンな知識とセンスで C++ を書けるように。 ◆ 後半: 近年の C++ 規格策定の動向 • C++ 規格マニアのためのトピックを 8 つ紹介。 • 講演を聞くことで、C++ 標準化の流れを追えるようになり、 将来の C++ の進化を見据えたソフトウェア・API 設計ができるように。 2
最新の C++ を解説する オープンソースの日本語 Web サイトを作っています cpprefjp cpprefjp.github.io • 標準ライブラリや言語機能のリファレンスとサンプル • 各規格における新機能のリストアップ cppmap cppmap.github.io • C++ の書籍やイベント、開発ツールなどの情報 3
近年の C++ の進化の流れ 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 C++11 C++14 C++17 C++20 C++23 標準化後の C++ の初メジャーアップデート。モダン C++ の基礎を築く • 範囲 for • auto • スマートポインタ • nullptr • ラムダ式 • 並行処理ライブラリ • constexpr • ムーブセマンティクス 4
近年の C++ の進化の流れ 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 C++11 C++14 C++17 C++20 C++23 C++11 の改良版。既存機能を改良するマイナーアップデートが中心 • ジェネリックラムダ • 戻り値型推論 • 変数テンプレート • 桁区切り文字 5
近年の C++ の進化の流れ 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 C++11 C++14 C++17 C++20 C++23 コードを簡潔にする文法や標準ライブラリの拡充 • 構造化束縛 • std::optional • std::variant • インライン変数 6
近年の C++ の進化の流れ 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 C++11 C++14 C++17 C++20 C++23 モダンなプログラミング手法を取り入れた大規模アップデート • コンセプト • ビット操作 • 三方比較演算子 • 指示付き初期化 • Ranges • std::format • モジュール • コルーチン 7
近年の C++ の進化の流れ 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 C++11 C++14 C++17 C++20 C++23 C++20 の新機能をさらに発展させる補完的なアップデート • std::print • std::expected • 添字演算子の多次元サポート • 標準ライブラリモジュール 8
過去(CEDEC 2020)講演との関係 前回の講演 C++11 C++14 C++17 C++20 C++23 9
過去(CEDEC 2020)講演との関係 今回の講演 C++11 C++14 C++17 C++20 C++23 10
仕事場に最新の C++ が来ていなくても・・・ • 新機能は、従来の文法やライブラリ機能の欠点・間違いやすさを改善。 現在使っている C++ の限界や罠に気付くことができる。 • 新しいライブラリ機能を試せる先行実装がある。 現在のバージョンで活用したり、将来に備えた設計の準備ができる。 • 新しいバージョンの C++ は必ずやってくる。 • プログラミング言語がどのように進化・改良されているか、 言語設計の思想を学ぶことで、プログラミング全般への理解が深まる。 11
新しいコア言語機能 1 size_t 型を表現するリテラルを活用しよう 2 ヘッダの有無を調べたいときは __has_include を使おう 3 状態をもたない関数オブジェクトの operator() を static にしよう 4 コンストラクタに [[nodiscard]] を指定しよう 5 範囲 for 文における一時オブジェクトの寿命に気を付けよう 6 関数の引数をコンパイル時にチェックできる方法を知ろう 7 標準コンセプトを活用しよう 8 制約を使ったときのオーバーロード解決ルールを知ろう 12
新しい標準ライブラリの機能(Range 以外)1/2 9 列挙型から整数型への変換に std::to_underlying() を使おう 10 ある文字や文字列が含まれているかを .contains(x) で調べよう 11 .resize_and_overwrite() で std::string のコストをさらに抑えよう 12 符号無し整数型と符号付き整数型の値の比較を安全に行う方法を知ろう 13 std::optional::value_or() での不要なコストに気を付けよう 14 std::optional に追加されたモナディックな操作を知ろう 15 std::optional で不足するなら std::expected を使おう 16 配列が特定の並びで開始/終了しているかを調べる方法を知ろう 13
新しい標準ライブラリの機能(Range 以外)2/2 17 連続するメモリの範囲は std::span で表現しよう 18 一次元配列を多次元配列のように扱いたいときは std::mdspan を使おう 19 std::format() の書式文字列のコンパイル時エラーチェックを知ろう 20 配列やコンテナの文字列化を std::format() に任せよう 21 ユーザ定義型を std::format() に対応させる方法を知ろう 22 ファイルを排他的に作成する方法を知ろう 23 エラーの調査にスタックトレースオブジェクトを活用しよう 24 非推奨化/削除される機能に注意しよう 14
新しい標準ライブラリの機能(Range)1/2 25 Range アダプタを使ってみよう 26 配列への逆順や部分アクセスに範囲 for 文を使おう 27 Range アダプタのパイプライン記法を使いこなそう 28 ループでインデックス値が必要なときも範囲 for 文が使えることを知ろう 29 複数のコンテナの要素を対応付けて扱うときも範囲 for 文を使おう 30 指定した区切り文字での文字列分割には std::views::split() を使おう 31 複数の集合の組み合わせを 1 つの範囲 for 文で簡単に生成しよう 32 単調増加や繰り返しの Range を手軽に作成する方法を知ろう 15
新しい標準ライブラリの機能(Range)2/2 33 集合を等サイズのグループに分割するときは std::views::chunk() を使おう 34 1 要素ずつずらしながらグループを作るときは std::views::slide() を使おう 35 タプルを持つコンテナの範囲 for 文では必要なメンバアクセスだけ抽出しよう 16
近年の C++ 規格策定の動向 36 C++ の新しい規格が決まる過程 37 最新の C++ 標準化の状況を調べる方法 38 ネットワーク機能と非同期処理の紆余曲折 39 契約プログラミング導入はどうなっている? 40 パターンマッチング導入はどうなっている? 41 リフレクション導入はどうなっている? 42 最新の C++ を先取りする方法 43 ゲーム業界から C++ 標準化委員会への提言 17
1 size_t 型を表現する リテラルを活用しよう 18
size_t 型を表現するリテラルを活用しよう size_t は環境依存の型。std::max() 等でのリテラルの扱いに注意が必要だった。 • unsigned int size_t の正体候補 • unsigned long • unsigned long long size_t size = str.length(); max(size, 5); size_t は int ではないのでコンパイルエラー max(size, 5ul); max<size_t>(size, 5); size_t が unsigned long でない環境でコンパイルエラー OK だが、明示的な size_t の記述が必要 19
size_t 型を表現するリテラルを活用しよう C++23 から、size_t 型の整数リテラルを表現するサフィックス uz が追加。 size_t 型の使い勝手が基本型に近づいた。 size_t size = str.length(); max(size, 5uz); // どちらも size_t 型 auto i = 0uz; // size_t 型 20
2 ヘッダの有無を調べたいときは __has_include を使おう 21
ヘッダの有無を調べたいときは __has_include を使おう あるヘッダの存在の有無に応じて使用するコードを分けたいときがある。 • 【例 1】新しい C++ 標準ライブラリのヘッダが存在するかどうか • 【例 2】サードパーティーライブラリのヘッダに対してパスが通っているか C++17 以降、プリプロセッサ ディレクティブ __has_include(〇〇) が使える。 インクルードファイル 〇〇 が存在する場合は 1, 存在しない場合 0 になる。 #if __has_include(〇〇) // ヘッダファイル 〇〇 が存在する場合 #else // ヘッダファイル 〇〇 が存在しない場合 #endif 22
ヘッダの有無を調べたいときは __has_include を使おう
【使用例 1】新しい C++ 標準ライブラリヘッダが無いとき、代替実装を使う。
#if __has_include(<span>)
#include <span> // 標準ライブラリ
using std::span;
#else
#include <gsl/span> // 代替のライブラリ
using gsl::span;
#endif
void f(span<int> s)
{
// ...
}
23
ヘッダの有無を調べたいときは __has_include を使おう
【使用例 2】特定のサードパーティライブラリを利用可能な(ヘッダにパスが通ってい
る)とき、拡張機能を提供する。
#if __has_include(<opencv2/opencv.hpp>)
#include <opencv2/opencv.hpp> // OpenCV へのパスが通っている場合
namespace mylib
{
// 自作クラスを OpenCV のクラスへ変換する関数
cv::Mat ConvertToMat(const Image&);
}
#endif
24
3 状態をもたない関数オブジェクトの operator() を static にしよう 25
状態をもたない関数オブジェクトの
operator() を static にしよう
関数オブジェクトは関数ポインタに比べてインライン化しやすく、コンパイラの最適化
を促進する。
struct CompObj {
bool operator()(int) const;
};
関数オブジェクト
int F(const vector<int>& v) {
return ranges::count_if(v, CompObj{});
}
しかし、operator() はメンバ関数なので、インライン化されなかった場合には、this
ポインタを受け渡す追加の小さなコストが生じる。
26
状態をもたない関数オブジェクトの
operator() を static にしよう
C++23 では operator() が this を使わない(状態を持たない)場合、static とし
て宣言して最適化を促進できるように。
struct CompObj {
static bool operator()(int);
};
int F(const vector<int>& v) {
return ranges::count_if(v, CompObj{});
}
27
状態をもたない関数オブジェクトの operator() を static にしよう 生成されるコードの変化(GCC, -O2): F(std::vector<int, std::allocator<int> > const&): push r13 push r12 push rbp push rbx sub rsp, 24 mov r12, QWORD PTR [rdi+8] mov rbx, QWORD PTR [rdi] cmp r12, rbx je .L4 xor ebp, ebp .L3: mov esi, DWORD PTR [rbx] lea rdi, [rsp+15] add rbx, 4 call CompObj::operator()(int) const movzx eax, al add rbp, rax cmp r12, rbx jne .L3 add rsp, 24 mov eax, ebp pop rbx pop rbp pop r12 pop r13 ret .L4: add rsp, 24 xor eax, eax pop rbx pop rbp pop r12 pop r13 ret F(std::vector<int, std::allocator<int> > const&): push r12 push rbp push rbx mov r12, QWORD PTR [rdi+8] mov rbx, QWORD PTR [rdi] cmp r12, rbx je .L4 xor ebp, ebp .L3: mov edi, DWORD PTR [rbx] add rbx, 4 call CompObj::operator()(int) movzx eax, al add rbp, rax cmp r12, rbx jne .L3 mov eax, ebp pop rbx pop rbp pop r12 ret .L4: pop rbx xor eax, eax pop rbp pop r12 ret 28
4 コンストラクタに [[nodiscard]] を指定しよう 29
コンストラクタに [[nodiscard]] を指定しよう C++20 から、通常の関数・メンバ関数だけでなく、コンストラクタにも [[nodiscard]] 属性を付与できるようになった。ほとんどのクラスは「オブジェクト を構築しただけで使わない」ということはないため、[[nodiscard]] の付与が誤った コードの検出に寄与。特に関数とコンストラクタの取り違え防止に役立つ。 struct Rect { [[nodiscard]] Rect(int x, int y, int w, int h); }; int main() { Rect(100, 100, 400, 200); // 警告: 構築したオブジェクトを無視 } 30
5 範囲 for 文における 一時オブジェクトの寿命に気を付けよう 31
範囲 for 文における一時オブジェクトの寿命に気を付けよう 一時オブジェクトに対する直接的な範囲 for ループを記述したとき、一時オブジェクト の寿命はループの終了まで延長される。次のコードは期待通り動く。 vector<string> GetMessages(); int main() { for (const auto& message : GetMessages()) { } } 32
範囲 for 文における一時オブジェクトの寿命に気を付けよう しかし C++20 時点では、一時オブジェクトそのものではなく、内部の要素に対する範 囲 for ループを記述した場合、元の一時オブジェクトの寿命は延長されず、未定義動作 (ダングリング)となっていた。 vector<string> GetMessages(); int main() { for (const auto& ch : GetMessages()[0]) { } } 33
範囲 for 文における一時オブジェクトの寿命に気を付けよう C++20 およびそれ以前では、この問題を回避するために、一時オブジェクトを変数で受 け、寿命を延長させる必要があった。 vector<string> GetMessages(); int main() { for (const auto& messages = GetMessages(); const auto& ch : messages[0]) { } } 寿命がループ終了まで延長 34
範囲 for 文における一時オブジェクトの寿命に気を付けよう C++23 では仕様が変更され、for (auto elem : ■■) の ■■ で生じた一時オブ ジェクトの寿命は、ループの終了まで延長されるようになった。 C++23 からは、範囲 for 文を書く際の注意事項が減った。 vector<string> GetMessages(); int main() { for (const auto& ch : GetMessages()[0]) { } } OK に 35
6 関数の引数をコンパイル時に チェックできる方法を知ろう 36
関数の引数をコンパイル時にチェックできる方法を知ろう
関数の引数が必ずコンパイル時定数として記述される場合、その値の妥当性もコンパイ
ル時にチェックできると、コーディングミスを早期発見でき嬉しい。
【例】0~100 の範囲の引数を渡さないといけない関数:
void SaveJPEG(std::string_view path, int quality);
int main() {
SaveJPEG("a.jpg", 300);
SaveJPEG("b.jpg", -200);
SaveJPEG("c.jpg", 80);
}
// 実行時エラー
// 実行時エラー
リテラルはコンパイル時定数なので、
コンパイル時にチェックする余地があるはず
37
関数の引数をコンパイル時にチェックできる方法を知ろう
consteval 指定された関数やコンストラクタは必ずコンパイル時評価され、途中でコン
パイル時評価できないコード(throw など)に達するとコンパイルエラーになる。これ
を利用して引数をラップしたクラスを作ると、コンパイル時引数チェックが実現。
struct JPEGQuality { int value;
consteval JPEGQuality(int n) : value{ n } {
if (n < 0 || 100 < n) throw "Invalid JPEG quality (must be 0-100)"; }
};
void SaveJPEG(std::string_view path, JPEGQuality quality);
int main() {
SaveJPEG("a.jpg", 300);
// コンパイル時エラー
SaveJPEG("b.jpg", -200); // コンパイル時エラー
SaveJPEG("c.jpg", 80);
// OK
}
38
7 標準コンセプトを活用しよう 39
標準コンセプトを活用しよう C++20 から、コンセプトを使用してテンプレートに型制約を与えられるようになった。 テンプレートの意図を明確にし、コードの安全性と可読性を向上させられる。 標準ライブラリで提供されているコンセプトの例: 【例 1】整数型を示すコンセプト std::integral auto Add(integral auto a, integral auto b) { return a + b; } 40
標準コンセプトを活用しよう
【例 2】浮動小数点数型を示すコンセプト std::floating_point
template <floating_point Float>
struct Vector4D
{
Float x, y, z, w;
};
Vector4D<float> や Vecotr4D<double> のみを許容し、
Vector4D<int> や Vector4D<std::string> を禁止できる。
41
標準コンセプトを活用しよう 【例 3】乱数生成器を示すコンセプト std::uniform_random_bit_generator 第二引数には std::mt19937 や std::random_device ほか、自作した乱数生成器ク ラスのみを渡せるよう制約。 template <class T> const T& Choice(const vector<T>& v, uniform_random_bit_generator auto&& rng); 次のように乱数生成器を渡さないオーバーロードとも共存可能。 乱数生成器ではなく // 配列から n 個ランダムに取得する 整数を渡すオーバーロード template <class T> vector<T> Choice(const vector<T>& v, size_t n); 42
8 制約を使ったときの オーバーロード解決ルールを知ろう 43
制約を使ったときのオーバーロード解決ルールを知ろう
制約付き関数のオーバーロード解決にあたっては、2 つの基本ルールがある。
【ルール 1】&& からなる制約では、より多く制約しているものが選ばれる。
template <class T> requires integral<T> && (sizeof(T) == 4)
void F(T) { cout << "A\n"; }
template <class T> requires integral<T>
void F(T) { cout << "B\n"; }
int main() {
F(1);
// A
F(10ULL); // B
requires
果物 && 赤い
requires
果物
F(10.0f); // コンパイルエラー
}
44
制約を使ったときのオーバーロード解決ルールを知ろう
【ルール 2】 || からなる制約では、最も制約の少ないものが選ばれる。
template <class T> requires integral<T>
void F(T) { cout << "A\n"; }
template <class T> requires integral<T> || (sizeof(T) == 4)
void F(T) { cout << "B\n"; }
int main() {
F(1);
// A
F(10u);
// A
requires
果物 || 赤い
requires
果物
F(10.0f); // B
}
45
9 列挙型から整数型への変換に std::to_underlying() を使おう 46
列挙型から整数型への変換に std::to_underlying() を使おう 列挙型の値 e から整数値を得るとき、これまでは static_cast<整数型>(e) がよく使 われていたが、次のようなトラブルが起こりえた。 ① enum class : uint8_t があり、static_cast<uint8_t> で変換していたが、 基底型が uint16_t に変更され、256 以上の列挙子が追加された → 256 以上の列挙子の変換が不正確に。警告なし。 ② enum class を static_cast<int> で変換していたが、 基底型が uint32_t に変更された → 0x80000000 以上を変換すると負の値に。警告なし。 47
列挙型から整数型への変換に std::to_underlying() を使おう 基底型を調べる std::underlying_type_t<Enum> と組み合わせて static_cast することで、不正な型への変換を防げたが、記述が長かった。 auto i = static_cast<underlying_type_t<Enum>>(e); C++23 では std::to_underlying(e) にまとめ、不正な変換防止と簡潔さを両立。 auto i = to_underlying(e); // i は e の基底型 48
列挙型から整数型への変換に
std::to_underlying() を使おう
【よくあるパターン】列挙子に整数を足して別の列挙子を得る:
enum class Sampler {
Sampler0, Sampler1, Sampler2, Sampler3
};
Sampler s = static_cast<Sampler>(to_underlying(Sampler::Sampler0) + i);
enum class の値はデフォルトでは + 演算ができない。std::to_underlying で整数
化して計算し、再び元の型に戻す。
49
10 ある文字や文字列が含まれているかを .contains(x) で調べよう 50
ある文字や文字列が含まれているかを
.contains(x) で調べよう
C++23 では、std::string や std::string_view の文字列中に、ある文字や文字
列が含まれているかを調べるメンバ関数 .contains(x) が追加された。
npos との比較が必要だった従来の方法に比べ、直感的な記述になった。
if (str.find('C') != string::npos) { }
記述が冗長
if (str.find("C++") != string::npos) { }
if (str.contains('C')) { }
シンプル
if (str.contains("C++")) { }
51
11 .resize_and_overwrite() で std::string のコストをさらに抑えよう 52
.resize_and_overwrite() で std::string のコストをさらに抑えよう std::string には隠れたコストが存在する。 【問】次のような機能をもつ関数をどのように実装する? • str を count 回繰り返した文字列を返す • 例: Repeat("abc", 4) → "abcabcabcabc" string Repeat(const string& str, size_t count) { } 53
.resize_and_overwrite() で
std::string のコストをさらに抑えよう
【解答例】(30 点)ループで毎回 append
string Repeat(const string& str, size_t count)
{
string result;
for (size_t i = 0; i < count; ++i)
{
// capacity を毎回チェックするうえ、足りない場合はメモリを再確保
result.append(str);
}
return result;
}
54
.resize_and_overwrite() で
std::string のコストをさらに抑えよう
【解答例】(80 点)reserve しておく
string Repeat(const string& str, size_t count)
{
string result;
result.reserve(str.size() * count); // 必要なメモリをあらかじめ確保しておく
for (size_t i = 0; i < count; ++i)
{
// capacity を毎回チェックする
result.append(str);
}
return result;
}
55
.resize_and_overwrite() で
std::string のコストをさらに抑えよう
【解答例】(90 点)resize してからの memcpy
string Repeat(const string& str, size_t count)
{
const size_t step_size = str.size();
string result;
// 必要なメモリをあらかじめ確保しておく
result.resize(step_size * count);
あとで上書きするのに要素は 0 埋めされる
for (size_t i = 0; i < count; ++i)
{
memcpy(result.data() + i * step_size, str.data(), step_size);
}
return result;
}
56
.resize_and_overwrite() で
std::string のコストをさらに抑えよう
【解答例】(100 点)C++23 の .resize_and_overwrite() を使用
string Repeat(const string& str, size_t count)
{
const size_t step_size = str.size();
string result;
result.resize_and_overwrite(step_size * count, [&](char* dst, size_t) {
for (size_t i = 0; i < count; ++i) {
memcpy(dst + i * step_size, str.data(), step_size);
}
return (step_size * count);
});
return result;
}
57
.resize_and_overwrite() で
std::string のコストをさらに抑えよう
.resize_and_overwrite() は、string を指定された長さでリサイズする際、要素
をゼロで初期化する代わりに、ユーザが渡した関数を使って初期値を埋める。
より低レイヤへのアクセスで、取り除くのが困難だったコストを削減できる。
必要なキャパシティ
ここでは 200
ラムダ式
string s;
s.resize_and_overwrite(200, [ ](char* dst, size_t capacity)
{
キャパシティ分確保されたバッファの先頭ポインタ。
dst;
バッファの要素は不定値状態なので、プログラムで値を書き込む
// ...
return 180;
最終的な文字列の長さ(capacity 以下)を報告する
});
cout << s.capacity() << '\n'; //(200 以上)
cout << s.size() << '\n'; // 180
58
12 符号無し整数型と符号付き整数型の 値の比較を安全に行う方法を知ろう 59
符号無し整数型と符号付き整数型の
値の比較を安全に行う方法を知ろう
int 型の値 vs size_t 型の値の比較は、意図しない暗黙型変換により一方の情報が欠落
することがあり安全でない。警告が発生することもある。
int n = -1;
unsigned int m = 1;
bool c = (n < m); // false
std::vector<int> v;
cv::Mat mat;
if (v.size() == mat.rows) { // size_t, int なので警告
}
60
符号無し整数型と符号付き整数型の
値の比較を安全に行う方法を知ろう
C++20 では、異なる整数型の値を安全に比較できる関数が追加された 。
a == b
std::cmp_equal(a, b)
a > b
std::cmp_greater(a, b)
a != b
std::cmp_not_equal(a, b)
a <= b
std::cmp_less_equal(a, b)
a < b
std::cmp_less(a, b)
a >= b
std::cmp_greater_equal(a, b)
int n = -1;
unsigned int m = 1;
bool c = std::cmp_less(n, m); // true
std::vector<int> v;
cv::Mat mat;
if (std::cmp_equal(v.size(), mat.rows)) { // 警告は出ない & 安全
}
61
13 std::optional::value_or() での 不要なコストに気を付けよう 62
std::optional::value_or() での 不要なコストに気を付けよう std::optional から値を取得する際、有効値を持っていない場合の代わりの値を指定 できるのが .value_or(x) メンバ関数。 引数 x は有効値の保持・不保持に関わらず評価されるため、必要のないケースにおいて も必ず評価され(x が関数の場合は関数が呼ばれ)、不要なコストが発生しうる。 optional<int> opt = 100; // 不要にも関わらず HeavyTask() が評価される int n = opt.value_or(HeavyTask()); 見かけの簡潔さに騙されないように 63
std::optional::value_or() での 不要なコストに気を付けよう .value_or(x) でのコストを回避するには、少し長くなるが次のように書く。 optional<int> opt = 100; // 必要な時だけ HeavyTask() が評価される int n = (opt.has_value() ? *opt : HeavyTask()); if を使っても良い if (opt.has_value()) n = *opt; else n = HeavyTask(); 64
14 std::optional に追加された モナディックな操作を知ろう 65
std::optional に追加されたモナディックな操作を知ろう
C++17 で導入された std::optional は便利だが、コード内に if ( ) や *value を
増やしがちだった。
if (optional<string> input = GetInput())
{
if (optional<int> result = Parse(*input))
{
cout << Square(*result) << '\n';
}
}
66
std::optional に追加されたモナディックな操作を知ろう
C++23 では、関数型プログラミングでよく使われる「モナド」の考え方を取り入れ、
std::optional 値への計算を連鎖させる仕組みを提供。
値への一連の操作をチェーン化し、if ( ) や *value の記述回数を減らす。
if (optional<int> result = GetInput()
.and_then(Parse).transform(Square))
{
cout << *result << '\n';
}
67
std::optional に追加されたモナディックな操作を知ろう std::optional::and_then(f) 有効値を保持していれば、std::optional を返す関数 f に値を渡した結果を返す。 有効値を保持していなければ std::nullopt を返す。 std::optional::transform(f) 有効値を保持していれば、通常の関数 f を値に適用した結果を std::optional に格 納して返す。 有効値を保持していなければ std::nullopt を返す。 std::optional::or_else(f) 有効値を保持していれば何もしない。 有効値を保持していなければ、 f() の結果を std::optional として返す。 68
15 std::optional で不足するなら std::expected を使おう 69
std::optional で不足するなら std::expected を使おう
C++17 から提供された std::optional<T> は「無効値」という状態を導入。
値を返す関数について、エラー時には無効値 std::nullopt を返せるようになった。
optional<int> ParseInt(string_view s) {
if (失敗時)
return nullopt;
else
return 123;
}
if (auto opt = ParseInt("123"))
cout << *opt;
70
std::optional で不足するなら std::expected を使おう
しかし、std::optional はエラーの原因等、詳細を返すことができなかった。
optional<int> ParseInt(string_view s) {
if (s.empty()) return nullopt;
エラーはすべて nullopt で表現
if (s に不正な文字) return nullopt;
if (結果が整数オーバーフロー) return nullopt;
return 123;
}
エラーを参照で受け取るといった方法が必要だった。
optional<int> ParseInt(string_view s, ErrorReason& error);
71
std::optional で不足するなら std::expected を使おう
C++23 の std::expected<T, E> は「正常値またはエラー値」を格納する。
指定した型でエラー値を表現できるようになった。
expected<int, ErrorReason> ParseInt(string_view s) {
if (s.empty()) return unexpected(ErrorReason::EmptyInput);
if (s に不正な文字) return unexpected(ErrorReason::InvalidCharacter);
if (結果が整数オーバーフロー) return unexpected(ErrorReason::IntegerOverflow);
return 123;
}
if (auto result = ParseInt("123"))
cout << *result;
else
ErrorReason reason = result.error();
72
std::optional で不足するなら std::expected を使おう std::expected の特徴 • ヒープから動的にメモリを確保しない • 正常値とエラー値はメモリを共有する • 通常、sizeof(expected<T, E>) <= (sizeof(T) + sizeof(E)) • std::optional と似たインタフェースを持ち、習得が容易 • explicit operator bool() • .has_value() • .value_or() • .error() • operator* • operator -> 73
16 配列が特定の並びで開始/終了 しているかを調べる方法を知ろう 74
配列が特定の並びで開始/終了
しているかを調べる方法を知ろう
「配列が特定の要素の並びで始まっているか/終わっているか」を調べる処理は、配列
の長さに注意が必要で、少し複雑だった。
【例】バイナリデータのヘッダ判定(バイト列の先頭 3 バイトの確認):
// バイナリデータ
vector<unsigned char> blob = { ... };
// パターン
constexpr array<unsigned char, 3> pattern = { 0x01, 0x02, 0x03 };
// 配列が指定したパターンで始まっているか
bool result = (pattern.size() <= blob.size())
&& equal(pattern.begin(), pattern.end(), blob.begin());
75
配列が特定の並びで開始/終了
しているかを調べる方法を知ろう
C++20 では std::string に .starts_with(x) / .ends_with(x) が入った。
C++23 では汎用バージョンの std::ranges::starts_with(x) / ends_with(x)
が提供される。
// 配列
vector<unsigned char> blob = { ... };
// パターン
constexpr array<unsigned char, 3> pattern = { 0x01, 0x02, 0x03 };
// 配列が指定したパターンで始まっているか
bool result = ranges::starts_with(blob, pattern);
76
17 連続するメモリの範囲は std::span で表現しよう 77
連続するメモリの範囲は std::span で表現しよう
メモリ連続な範囲は、配列や std::vector などさまざまな型で表現される。
それらを関数に渡す場合、ポインタと要素数の 2 つを渡すパターンが使われた。
void F(const int* p, size_t count) {
for (size_t i = 0; i < count; ++i) cout << p[i] << ' ';
cout << '\n';
}
int main() {
int a[10] = {};
array<int, 10> b{};
vector<int> c(10);
F(a, size(a));
F(b.data(), b.size());
F(c.data(), c.size());
}
78
連続するメモリの範囲は std::span で表現しよう
std::span<T> は std::string_view の配列版。メモリ連続な範囲を一貫した方法
で受け渡しできるようになる。T への const の有無で要素の変更可否も制御できる。
void F(span<const int> s) {
for (int n : s) cout << n << ' ';
cout << '\n';
}
int main() {
int a[10] = {};
array<int, 10> b{};
vector<int> c(10);
F(a);
F(b);
F(c);
}
79
18 一次元配列を多次元配列のように 扱いたいときは std::mdspan を使おう 80
一次元配列を多次元配列のように 扱いたいときは std::mdspan を使おう 画像などの二次元配列的な情報を一次元配列上で表現する場合、指定した要素にアクセ スする際のインデックスの計算が面倒だった。 constexpr int W = 1280, H = 720; vector<float> pixels(W * H); pixels[0 * W + 0] = 0.5f; pixels[360 * W + 640] = 1.0f; int w = 256, h = 192; vector<float> values(w * h); values[0 * w + 0] = 0.5f; values[96 * w + 128] = 1.0f; 81
一次元配列を多次元配列のように
扱いたいときは std::mdspan を使おう
C++23 の std::mdspan を使うと、一次元配列を二次元配列や三次元配列であるかの
ように見せるインタフェースを持つビューが提供される。
constexpr int W = 1280, H = 720;
サイズがコンパイル時定数の場合
vector<float> pixels(W * H);
mdspan<float, extents<int, H, W>> image{ pixels.data() };
image[0, 0] = 0.5f;
image[360, 640] = 1.0f;
C++23 から [] 内に複数記述可能
int w = 256, h = 192;
vector<float> values(w * h);
mdspan<float, dextents<int, 2>> view{ values.data(), h, w };
view[0, 0] = 0.5f;
view[96, 128] = 1.0f;
サイズが実行時に決まる場合
82
19 std::format() の書式文字列の コンパイル時エラーチェックを知ろう 83
std::format() の書式文字列の コンパイル時エラーチェックを知ろう C++20 の std::format() は、書式文字列(文字列リテラルなどの定数式)をコンパ イル時計算でパースする( 6 も参照)。書式の誤りを、実行する前にコンパイルエ ラーとして検出できるため、コード実行の安全性が高まる。 string s; s = format("{ {}", 123); // エラー: 不正な括弧 s = format("{}, {}", 123); s = format("{2}", 123); s = format("{} {1}", 1, 3); s = format("{:d}", "123"); // エラー: 引数の不足 // エラー: 引数の不足 // エラー: インデックス指定の有無の混在 // エラー: 引数の型の不一致 84
20 配列やコンテナの文字列化を std::format() に任せよう 85
配列やコンテナの文字列化を std::format() に任せよう
これまで std::vector や std::map を文字列化する標準の方法は用意されていなかっ
たため、自前での変換が必要だった。
string ToString(const vector<int>& v) {
string result = "[";
for (size_t i = 0; i < v.size(); ++i) {
if (0 < i)
result += ", ";
result += to_string(v[i]);
}
result += "]";
return result;
}
86
配列やコンテナの文字列化を std::format() に任せよう
C++23 から、配列や各種コンテナを std::format で文字列化できるようになった。
int a[] = { 1, 2, 3 };
cout << format("{}", a) << '\n'; // [1, 2, 3]
vector<int> v = { 10, 20, 30 };
cout << format("{}", v) << '\n'; // [10, 20, 30]
map<string, int> m = { { "one", 1 }, { "two", 2 }, { "three", 3 } };
cout << format("{}", m) << '\n'; // {"one": 1, "three": 3, "two": 2}
pair<double, double> p = { 1.11, 2.22 };
cout << format("{}", p) << '\n'; // (1.11, 2.22)
87
21 ユーザ定義型を std::format() に 対応させる方法を知ろう 88
ユーザ定義型を std::format() に対応させる方法を知ろう
これまでの C++ では、ユーザ定義型を出力ストリームに対応させる場合、
std::ostream に対する operator << をオーバーロードした。
struct Vector2D {
double x, y;
friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
};
89
ユーザ定義型を std::format() に対応させる方法を知ろう
C++20 の std::format() へ対応するには、std::formatter の特殊化を実装する。
template<>
struct std::formatter<Vector2D> {
template <class ParseContext>
constexpr auto parse(ParseContext& ctx) {
return ctx.begin();
{:.4} のようなオプション書式にも
}
対応したい場合、ここに詳細な実装
template <class FormatContext>
auto format(const Vector2D& v, FormatContext& ctx) const {
return std::format_to(ctx.out(), "({}, {})", v.x, v.y);
}
};
90
ユーザ定義型を std::format() に対応させる方法を知ろう
std::format() に対応することで書けるコードの例:
int main()
{
std::string s = std::format("{}", Vector2D(1.11, 2.22));
std::println("{}", Vector2D(12.34, 34.56));
}
(12.34, 34.56)
91
22 ファイルを排他的に 作成する方法を知ろう 92
ファイルを排他的に作成する方法を知ろう
「ファイルへの書き込みをするが、既にファイルがあれば書き込みはしない」という排
他的アクセスを実現したいとき、次のように書くと、(1) の瞬間に外部からファイルを作
成されて不適切な競合(TOCTOU)が生じる余地があった。
void WriteFile(filesystem::path filename) {
if (filesystem::exists(filename)) { // ファイルの存在をチェック
LOG("ファイルがすでに存在しています。");
return;
}
// (1)
ofstream ofs{ filename, ios_base::out };
// ... 書き込み処理
}
93
ファイルを排他的に作成する方法を知ろう
このような競合を防ぐため、排他モードのフラグとして、POSIX には O_EXCL,
Windows API には CREATE_NEW などが用意されている。C++23 からは、C++ 標準
ファイルストリームにも std::ios_base::noreplace フラグが追加された。
void WriteFile(filesystem::path filename) {
ofstream ofs{ filename, ios_base::out | ios_base::noreplace };
if (!ofs) {
LOG("ファイルがすでに存在しています。");
return;
}
// ofs がこの関数で新規作成されたファイルであることが保証されている
// ... 書き込み処理
}
94
23 エラーの調査にスタックトレース オブジェクトを活用しよう 95
エラーの調査にスタックトレースオブジェクトを活用しよう
デバッガをアタッチしていない環境で問題が発生した場合、assert では十分な情報を
得られないことがある。C++23 で導入された標準の <stacktrace> ライブラリを使う
ことで、一貫した方法でスタックトレースを取得できるようになった。
using Error = pair<string, stacktrace>;
void Nest2() { throw Error{ "Error123", stacktrace::current() }; }
void Nest1() { Nest2(); }
void F() {
Error123
try { Nest1(); }
catch (const Error& e)
{ println("{}\n{}", e.first, e.second); }}
int main() {
F();
0> Main.cpp(2): MyApp!Nest2+0x37
1> Main.cpp(3): MyApp!Nest1+0x21
2> Main.cpp(5): MyApp!F+0x32
3> Main.cpp(9): MyApp!main+0x21
}
96
24 非推奨化/削除される 機能に注意しよう 97
非推奨化/削除される機能に注意しよう C++ で昔から提供されている標準ライブラリ機能について、次のような理由で別の方法 への置き換えが望ましくなることがある。 • 使い勝手や安全性に問題がある • 存在理由がほとんどない • コア言語機能で対応可能になった 上記の理由がとくに強い場合、段階を踏んで C++ の仕様から削除される。 ステップ 1: 非推奨化(deprecated)し、使用時に警告やエラーを出す 特定のマクロを定義することで警告をオプトアウトできる場合もある ステップ 2: 標準ライブラリから削除し、完全に使えなくなる 98
非推奨化/削除される機能に注意しよう 具体例と対処方法 ① std::auto_ptr は C++11 で非推奨化、C++17 で削除。 → std::unique_ptr を使う。 スマートポインタの前身 std::auto_ptr は、ムーブセマンティクスが無い時代の設計の ため、コピー演算子の挙動が非直感的で間違いやすかった。 std::result_of は C++17 で非推奨化、C++20 で削除。 → std::invoke_result を使う。 間違いやすさの解消や、標準ライブラリの他の機能との名前・使い勝手の一貫性のため。 99
非推奨化/削除される機能に注意しよう 具体例と対処方法 ② std::not1(), std::not2() は C++17 で非推奨化、C++20 で削除。 → std::not_fn() を使う。 アルゴリズム関数等に渡す、bool を返す関数(predicate)を反転させる。C++11 の可 変引数テンプレートを使った std::not_fn が任意個の引数に対応し、不要になった。 <codecvt> は C++17 で非推奨化、C++26 で削除。 → 代替手段無し。自前の Unicode 変換ライブラリを用いる。 標準で規定された仕様が安全ではなく、別の実装のほうが好ましいと判断された。 100
25 Range アダプタを使ってみよう 101
Range アダプタを使ってみよう
Range アダプタは従来の範囲 for 文をパワーアップし、変換、フィルタリング、サブ
セット化などの操作を可能にする。具体的には Range(配列やコンテナ)から新しい
ビューを生成し、そのビューへの範囲 for 文を書ける。
【例】views::reverse
入力された Range を逆順にアクセスするビューを作る。
vector<int> v = { 10, 20, 30, 50 };
for (const auto& n : v | views::reverse) {
cout << n << '\n';
}
50
30
20
10
ビューは遅延評価。ここでは逆順に並び替えた新しい配列を作るわけではなく、必要な
ときに評価され、余分なオーバーヘッドは生じない。
102
Range アダプタを使ってみよう
【例】views::transform
入力された Range の各要素を、指定した関数で変換した結果のビューを作る。
【例】views::filter
入力された Range のうち、指定した条件を満たす要素だけからなるビューを作る。
ビューは値として扱える
vector<int> v = { 1, 5, 10, 50, 100, 500 };
auto view1 = v | views::filter(IsEven);
auto view2 = view1 | views::transform(Square);
cout << format("{}", view2) << '\n';
[100, 2500, 10000, 250000]
103
26 配列への逆順や部分アクセスに 範囲 for 文を使おう 104
配列への逆順や部分アクセスに範囲 for 文を使おう
C++17 まで、範囲 for 文は全要素へ順番にアクセスするときにしか使えず、逆順や部
分配列では従来のループを記述する必要があった。
for (const auto& elem : v) {
cout << elem << '\n';
}
// 逆順
for (auto it = v.rbegin(); it != v.rend(); ++it) {
cout << *it << '\n';
}
// 最初の 2 つをスキップ
for (size_t i = 2; i < v.size(); ++i) {
cout << v[i] << '\n';
}
105
配列への逆順や部分アクセスに範囲 for 文を使おう C++20 / C++23 の Range アダプタを使うと、次のような変則的なアクセスパターンを、 実行時効率を落とさずに範囲 for 文で書けるようになる。 逆順 views::reverse 部分配列 views::drop(N) views::take(N) N 個おき views::stride(N) 106
配列への逆順や部分アクセスに範囲 for 文を使おう // 逆順 for (const auto& elem : v | views::reverse) { cout << elem << '\n'; } // 最初の 2 つをスキップ for (const auto& elem : v | views::drop(2)) { cout << elem << '\n'; } // 3 個おき for (const auto& elem : v | views::stride(3)) { cout << elem << '\n'; } 107
27 Range アダプタの パイプライン記法を使いこなそう 108
Range アダプタのパイプライン記法を使いこなそう C++20 / C++23 の Range アダプタは、| 演算子によるパイプライン記法で重ねがけす ることで、より複雑なビューを表現できる。 逆順 + 部分配列 views::reverse | views::take(N) 部分配列 + 部分配列 views::drop(N) | views::take(M) 逆順 + N 個おき views::reverse | views::stride(N) 109
Range アダプタのパイプライン記法を使いこなそう for (const auto& elem : v | views::reverse | views::take(6)) { // 逆順 + 6 つ cout << elem << '\n'; } for (const auto& elem : v | views::drop(2) | views::take(6)) { // 最初の 2 つをスキップ + 6 つ cout << elem << '\n'; } for (const auto& elem : v | views::reverse | views::stride(3)) { // 逆順 + 3 個おき cout << elem << '\n'; } 110
28 ループでインデックス値が必要なときも 範囲 for 文が使えることを知ろう 111
ループでインデックス値が必要なときも
範囲 for 文が使えることを知ろう
C++17 では、範囲 for 文でインデックス値を使いたい場合、範囲 for 文の外で変数を
作る必要があった。
vector<string> vs = { "zero", "one", "two", "three", "four" };
size_t i = 0;
for (const auto& s : vs) {
cout << i++ << ": " << s << '\n';
}
0: zero
1: one
2: two
3: three
4: four
5: five
112
ループでインデックス値が必要なときも
範囲 for 文が使えることを知ろう
C++20 では、範囲 for 文の ( ) 内で変数を宣言できるようになり、変数のスコープを
ループ内に制限できるようになった。
vector<string> vs = { "zero", "one", "two", "three", "four" };
for (size_t i = 0; const auto& s : vs) {
cout << i++ << ": " << s << '\n';
}
0: zero
1: one
2: two
3: three
4: four
5: five
113
ループでインデックス値が必要なときも
範囲 for 文が使えることを知ろう
C++23 の std::views::enumerate は、配列の要素とインデックス値をペアにした
ビューを提供する。構造化束縛を用いて次のように記述する。
vector<string> vs = { "zero", "one", "two", "three", "four" };
for (const auto& [i, s] : vs | views::enumerate) {
cout << i << ": " << s << '\n';
}
0: zero
1: one
2: two
3: three
4: four
5: five
114
29 複数のコンテナの要素を対応付けて 扱うときも範囲 for 文を使おう 115
複数のコンテナの要素を対応付けて
扱うときも範囲 for 文を使おう
2 つのコンテナについて、同時に先頭から順にアクセスし、それぞれの要素を組として
処理するパターンは、範囲 for 文だけで書くことができなかった。
vector<int> xs = { 1, 2, 3, 4 };
vector<string> ys = { "a", "b", "c" };
const size_t n = min(xs.size(), ys.size()); // 少ないほうに合わせる
for (size_t i = 0; i < n; ++i) {
cout << xs[i] << ": " << ys[i] << '\n';
}
1: a
2: b
3: c
116
複数のコンテナの要素を対応付けて
扱うときも範囲 for 文を使おう
std::views::zip() は、複数の Range の要素を対応付けた tuple からなるビューを
生成する。コンテナが複数あっても範囲 for 文が使えるようになった。
vector<int> xs = { 1, 2, 3, 4 };
vector<string> ys = { "a", "b", "c" };
for (auto&& [x, y] : views::zip(xs, ys)) {
cout << x << ": " << y << '\n';
}
ループ回数は少ないほうに合わせる
[x, y]
xs
1
2
3
ys
"a"
"b"
"c"
4
117
30 指定した区切り文字での文字列分割には std::views::split() を使おう 118
指定した区切り文字での文字列分割には
std::views::split() を使おう
設定ファイルのパース時など、文字列を指定した区切り文字で分割したいことがある。
Range アダプタ std::views::split() を使うことで、追加のメモリの確保無しで、
区切り後の結果に順にアクセスするビューを生成する。
string s = "10,20,30,40,aaa,bbb";
for (auto item : s | views::split(',')) {
cout << string_view{ item } << '\n';
}
10
20
30
aaa
bbb
119
指定した区切り文字での文字列分割には
std::views::split() を使おう
std::views::split() の引数には文字列を指定することもできる。ただし、文字列
リテラルを直接使うと、末尾のヌル文字までをも区切り文字列として扱ってしまう(例
えば "cc" は 「cc\0」)。代わりに std::string_view リテラルを使うとよい。
using namespace std::literals;
string s = "aa bb cc dd ee";
for (auto item : s | views::split("cc"sv)) {
cout << string_view{ item } << '\n';
}
aa bb
dd ee
120
31 複数の集合の組み合わせを 1 つの範囲 for 文で簡単に生成しよう 121
複数の集合の組み合わせを
1 つの範囲 for 文で簡単に生成しよう
複数の集合に対して、要素の組み合わせをすべて列挙する場合、範囲 for 文を集合の個
数だけネストさせる必要があった。
【例】2 つの集合 { " ", " ", " ", " " } と { 1, 2, …, 13 } から、すべてのトラン
プのカードを作る。
vector<string> suits = { " ", " ", " ", " " };
vector<int> ranks = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
for (const auto& suit : suits) {
for (const auto& rank : ranks) {
cout << suit << rank << '\n';
}
}
1
2
…
12
13
122
複数の集合の組み合わせを
1 つの範囲 for 文で簡単に生成しよう
C++23 の std::views::cartesian_product() は、複数の集合に対する組み合わ
せの全パターン(直積)を、1 つの範囲 for 文で挙げるビューを生成する。
vector<string> suits = { " ", " ", " ", " " };
vector<int> ranks = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
for (const auto& [suit, rank] : views::cartesian_product(suits, ranks)) {
cout << suit << rank << '\n';
}
1
2
…
12
13
123
32 単調増加や繰り返しの Range を 手軽に作成する方法を知ろう 124
単調増加や繰り返しの Range を
手軽に作成する方法を知ろう
std::views::iota(a, b) は、a から始まり b 未満まで 1 ずつ単調増加していく数
列を表現する Range を生成する。
for (int i : views::iota(100, 110)) {
cout << i << '\n';
}
100
101
102
103
104
105
106
107
108
109
125
単調増加や繰り返しの Range を
手軽に作成する方法を知ろう
第二引数を省略して std::views::iota(a) にすると、無限長の数列になる。
views::take(n) や views::zip() と組み合わせて、有限の Range として評価する。
vector<string> items = { "aaa", "bbb", "ccc", "ddd" };
for (const auto& [i, item] : views::zip(views::iota(100), items)) {
cout << i << ": " << item << '\n';
}
100: aaa
101: bbb
102: ccc
103: ddd
126
単調増加や繰り返しの Range を
手軽に作成する方法を知ろう
std::views::repeat(x, count) は、x を count 回繰り返す Range を生成する。
for (int i : views::repeat(-1, 5)) {
cout << i << '\n';
}
-1
-1
-1
-1
-1
127
単調増加や繰り返しの Range を
手軽に作成する方法を知ろう
第二引数を省略して std::views::repeat(x) にすると、無限回繰り返す。
views::zip() と組み合わせて、有限の Range として評価する。
vector<string> items = { "aaa", "bbb", "ccc", "ddd" };
for (const auto& [i, item] : views::zip(views::repeat(100), items)) {
cout << i << ": " << item << '\n';
}
100: aaa
100: bbb
100: ccc
100: ddd
128
33 集合を等サイズのグループに分割する ときは std::views::chunk() を使おう 129
集合を等サイズのグループに分割する ときは std::views::chunk() を使おう 配列の先頭から N 個ごとに切り分ける処理を、説明的なコードにするのは難しかった。 【例】所持している 28 個の素材(配列)に対して先頭から 5 個ずつグループ化して関 数に渡す(5 回 Craft() を呼ぶ) void Craft(span<const Material> group); int main() { vector<Material> materials(28); // ??? } 130
集合を等サイズのグループに分割する
ときは std::views::chunk() を使おう
通常のループを使って書いた例:
void Craft(span<const Material> group);
int main() {
vector<Material> materials(28);
for (size_t i = 0; i < (materials.size() / 5); ++i) {
Craft(span{ (materials.data() + i * 5), 5 });
}
}
131
集合を等サイズのグループに分割する
ときは std::views::chunk() を使おう
C++23 の std::views::chunk(N) は、集合を N 個ずつに切り分けた一連の部分配列
へのビューを生成する。N 個に満たない最後の余りも含まれる点には注意が必要。
void Craft(span<const Material> group);
int main() {
vector<Material> materials(28);
for (const auto& group : materials | views::chunk(5)) {
if (group.size() == 5) {
Craft(group);
}
}
}
132
集合を等サイズのグループに分割する ときは std::views::chunk() を使おう 対象となる要素数を std::views::take(M) であらかじめ切り出すと、余りが出なく なり、if を取り除ける。 void Craft(span<const Material> group); int main() { vector<Material> materials(28); for (const auto& group : materials | views::take(25) | views::chunk(5)) { Craft(group); } } 133
34 1 要素ずつずらしながらグループを作る ときは std::views::slide() を使おう 134
1 要素ずつずらしながらグループを作るときは std::views::slide() を使おう 配列の先頭 N 個でグループを作り、先頭が抜ける代わりに後ろが入ってグループを作り 直していく操作を、説明的なコードにするのは難しかった。 【例】8 人のパーティーメンバーの先頭から 4 人のグループを作り、1 人が抜けては後 ろの 1 人がグループに加入する(5 回 Game() を呼ぶ) void Game(span<const Character> group); int main() { vector<Character> members(8); // ??? } 135
1 要素ずつずらしながらグループを作るときは
std::views::slide() を使おう
通常のループを使って書いた例:
void Game(span<const Character> group);
int main() {
vector<Character> members(8);
if (4 <= members.size()) {
for (size_t i = 0; i <= (members.size() - 4); ++i) {
Game(span{ (members.data() + i), 4 });
}
}
}
136
1 要素ずつずらしながらグループを作るときは std::views::slide() を使おう C++23 の std::views::slide(N) を使うと、先頭から 1 要素ずつずらしながら N 要素で作るグループをすべて挙げるビューを簡単に生成できる。 void Game(span<const Character> group); int main() { vector<Character> members(8); for (const auto& group : members | views::slide(4)) { Game(group); } } 137
35 タプルを持つコンテナの範囲 for 文では 必要なメンバアクセスだけ抽出しよう 138
タプルを持つコンテナの範囲 for 文では
必要なメンバアクセスだけ抽出しよう
ペアやタプルの配列の走査で、一部のメンバにだけアクセスしたい場合、ループ本体で
無関係のメンバにアクセスしないようにするのは注意力が要求される。
vector<tuple<int, string, double>> v;
for (const auto& elem : v) {
cout << get<1>(elem) << '\n';
}
elem
int
get<0>(elem)
string
get<1>(elem)
double
get<2>(elem)
139
タプルを持つコンテナの範囲 for 文では
必要なメンバアクセスだけ抽出しよう
構造化束縛を利用しても、注意が必要なのは変わらない。
vector<tuple<int, string, double>> v;
for (const auto& [i, s, d] : v) {
cout << s << '\n';
}
[i, s, d]
int
i
string
s
double
d
140
タプルを持つコンテナの範囲 for 文では
必要なメンバアクセスだけ抽出しよう
C++20 の std::views::elements<I> を使うと、タプルのうち I 番目のメンバのみ
にアクセスするビューを生成でき、不要なメンバへのアクセスを避けられる。
vector<tuple<int, string, double>> v;
for (const auto& s : v | views::elements<1>) {
cout << s << '\n'; // string にアクセス
}
int
string
s
double
141
タプルを持つコンテナの範囲 for 文では
必要なメンバアクセスだけ抽出しよう
連想コンテナ用に
• std::views::elements<0> と同じ std::views::keys
• std::views::elements<1> と同じ std::views::values
があり、「Key のみ」「Value のみ」のアクセスも直感的に記述できる。
unordered_map<string, int> m = { { "one", 1 }, { "two", 2 } };
for (const auto& key : m | views::keys) {
cout << key << '\n';
全要素のキーのみにアクセス
}
142
タプルを持つコンテナの範囲 for 文では
必要なメンバアクセスだけ抽出しよう
抽出したビューに対しても、新しい Range アダプタをつなげられる。
vector<tuple<int, string, double>> v = {
{ 201, "apple", 1.1 },
{ 202, "banana", 2.2 },
{ 203, "kiwi", 3.3 },
{ 301, "strawberry", 4.4 },
6 文字以上のアイテムを、
{ 302, "grape", 5.5 }
0 から始まるインデックスとペアで列挙
};
for (const auto& [i, s] : v | views::elements<1>
| views::filter([](const string& s) { return 6 <= s.size(); })
| views::enumerate) {
cout << i << ": " << s << '\n';
0: banana
}
1: strawberry
143
近年の C++ 規格策定の動向 144
36 C++ の新しい規格が決まる過程 145
C++ の新しい規格が決まる過程 国際標準規格として発行 (3 年ごと) 承認 規格ドラフトにマージ C++ 標準化委員会 承認 CWG / LWG: 仕様文言を洗練させる 承認 EWG / LEWG: 設計を妥当なものにする 番号付き文書の提出・会議での発表 フォーラム、メーリングリストでの議論 専門家の集まるフォーラムに投稿 何かを思いつく 出典: https://isocpp.org/std/the-committee 146
37 最新の C++ 標準化の状況を調べる方法 147
最新の C++ 標準化の状況を調べる方法 • 新しいバージョンで追加された機能を学ぶ • cpprefjp • cppreference.com • 提案されている内容や、議論の状況を調べる • C++ 標準化委員会リポジトリの Issue トラッカー github.com/cplusplus/papers/issues • 提案ごとに Issue が立てられ、提案のカテゴリや状態、会議での投票結果などが記 録されている • 提案文書の日本語訳を公開しているブログ 講演者の 1 人が書いています • onihusube.hatenablog.com/archive/category/WG21 148
38 ネットワーク機能と 非同期処理の紆余曲折 149
ネットワーク機能と非同期処理の紆余曲折 • C++20 をターゲットに、標準ライブラリへのネットワーク機能が進んでいた。 • 一定の実績があったライブラリ Boost.Asio と、そこで使われていた非同期処理機構 Executor をセットで標準に導入しようとした。 • 2019 年夏、標準の Executor 提案(提案番号 P0443)の設計が完了し、C++23 には 入りそうな見込みとなった。 • Boost.Asio も、P0443 を前提としたネットワークライブラリをリリース、C++ 標準 化委員会でも P0443 に基づいたネットワークライブラリの作業が始まった。 • ところが、2021 年 6 月、P0443 を大きく改良した新 Executor ライブラリ(提案番 号 P2300)が提案された 150
ネットワーク機能と非同期処理の紆余曲折
using stdex = std::execution;
// excutor: 非同期処理を実行する場所(ハードウェア)を表現するもの
stdex::static_thread_pool pool(16);
stdex::executor auto ex = pool.executor();
// 引数無し戻り値なしの非同期処理
auto async_work = [&]() -> void {
cout << "In thread pool.\n";
...
};
// 非同期処理の開始、結果の受信とエラーハンドリングの方法が無い
stdex::execute(ex, async_work);
151
ネットワーク機能と非同期処理の紆余曲折 using stdex = std::execution; // scheduler: 非同期処理を実行する方法と完了する場所を表すもの stdex::static_thread_pool pool(16); stdex::scheduler auto sch = pool.scheduler(); // sender: 非同期処理グラフとその実行場所を表現するもの stdex::sender auto work = stdex::schedule(sch) | stdex::just(10) scheduler をパイプラインに組み込むことで、 sender が処理を実行する場所を表現する | stdex::then([](int n) { ... }) | stdex::then([](int n) { ... }) | ...; sender を受け、その処理を現在のスレッドで実行。 進捗をコールバックでき、結果やエラーをハンドルできる // 非同期処理の開始、完了を待機し完了後に結果を取り出して返す auto [result] = std::this_thread::sync_wait(work); 152
ネットワーク機能と非同期処理の紆余曲折 • C++ 的には、突如出てきた新提案 P2300 のほうが明確に優れていた(sender / receiver という抽象化モデルをコアに据え、汎用性が高まった) • 旧提案 P0433 をキャンセルし、新提案 P2300 を C++ 標準の Executor に採用しよ うという機運が高まった。 • そうすると、これまで旧提案 P0443 の Executor に基づいて開発してきた標準ネット ワークライブラリは、再設計を余儀なくされることに • 新提案 Executor P2300 は 2024 年 7 月に C++26 の規格案に正式にマージ • 一方、標準ネットワークライブラリのほうは、P2300 の Executor ベースで再度作り 直しに。標準ライブラリ入りは、もう少し時間がかかりそう 153
39 契約プログラミング導入は どうなっている? 154
契約プログラミング導入はどうなっている? • 2013 年頃から、C++ に契約プログラミングを導入する機運が高まる。 • 2018 年 6 月、契約プログラミング機能が C++20 に向けて採択される。 • 属性構文による事前 / 事後条件およびアサーションの指定(契約注釈) [[expects: expr]] [[ensures: expr]] [[assert: expr]] • ビルドモードによる契約注釈のセマンティクスの一括制御 • 契約条件を評価するかしないか / 契約違反が発生した際に継続するかしないか • 契約違反発生時は違反ハンドラによって最低限のメッセージを表示 • ビルドモードによって、その後継続するかが異なる 155
契約プログラミング導入はどうなっている? • しかし、C++20 への採択が決まったあと、細かい仕様を詰めていこうとしたら、いろ いろ問題があることが明らかになった • 契約違反が起きた後で継続できる必要はあるのか? • 契約注釈のセマンティクスを個別制御したい場合はどうするのか? • 「コンパイラが注釈を最適化のヒントとして利用できる」ことと、「契約が破られ た後も実行を継続するオプション」が競合(想定していないコードが実行され、未 定義動作を引き起こす) • 議論は紛糾し、2019 年 7 月には「C++20 までに間に合わない」となり、一旦 C++20 への採択を取り消すことに 156
契約プログラミング導入はどうなっている? • 2020 年~2023 年にかけ、コツコツと再設計が行われてきた。 • 契約プログラミングの基本部分を改めて固め直した: • 契約のための専用構文を、属性構文ではなく通常の構文に変更した。 • pre(expr), post(r: expr), contract_assert(expr); • 契約注釈の 4 種類のセマンティクス • コンパイラオプションとの対応 • 違反ハンドラのカスタマイズ • 現在も、機能追加要望や設計への疑問点がアクティブに議論されている。 • C++26 に間に合うかは微妙なところ。間に合わなかったら C++29。 157
契約プログラミング導入はどうなっている?
double Div(double a, double b)
[[expects: b != 0.0]]
// 事前条件
[[expects: isnan(a) == false]] // 事前条件
[[expects: isnan(b) == false]] // 事前条件
[[ensures r: isfinite(r)]]
// 事後条件
{
// axiom が指定された契約条件はチェックされない(最適化に利用される)
[[assert
: isinf(a) == false]]; // アサーション
[[assert axiom: isinf(b) == false]]; // アサーション
return a / b;
}
158
契約プログラミング導入はどうなっている?
double Div(double a, double b)
pre(b != 0.0)
// 事前条件
pre(isnan(a) == false) // 事前条件
pre(isnan(b) == false)
post(r: isfinite(r))
// 事前条件
// 事後条件
{
// 契約注釈はコードの仮定とされない
contract_assert(isinf(a) == false); // アサーション
contract_assert(isinf(b) == false); // アサーション
return a / b;
}
159
40 パターンマッチング導入は どうなっている? 160
パターンマッチング導入はどうなっている? • 2015 年頃から、C++ へのパターンマッチングの導入が検討されてきた • 2020 年に inspect 式を使うパターンマッチングの文法案が固まる(P1371) • 2021 年、別案として is / as キーワードを使ったパターンマッチング構文および is / as キーワードを使った C++ 文法の拡張案が発表される(P2392) • 2022 年、P1371 を修正し、inspect 式の代わりに match 式と let を用いた改善案 P2688 が提案された(match-let をパターンマッチングの文脈外でも使用できる) • 現在 P2392 と P2688 の 2 つの案が有望であるが、どちらの案も詳細が流動的であり、 最終判断を下せる状況には進んでいない。 • おそらく C++29 になりそう。 161
パターンマッチング導入はどうなっている?
// P2392R2 variant<int32_t, int64_t, float, double> のマッチング例
inspect (v) {
i32 as int32_t =>
std::print("got int32: {}", i32);
i64 as int64_t =>
std::print("got int64: {}", i64);
f as float =>
std::print("got float: {}", f);
d as double =>
std::print("got double: {}", d);
};
162
パターンマッチング導入はどうなっている?
// P2688R1 variant<int32_t, int64_t, float, double> のマッチング例
(v) match {
int32_t: let i32 =>
std::print("got int32: {}", i32);
int64_t: let i64 =>
std::print("got int64: {}", i64);
float: let f =>
std::print("got float: {}", f);
double: let d =>
std::print("got double: {}", d);
};
163
41 リフレクション導入はどうなっている? 164
リフレクション導入はどうなっている? • 2020 年、リフレクションの初期案(Reflection TS: N4856)が一旦まとまった。 • テンプレートメタプログラミングのように、リフレクション情報を「型」として扱う ものだった。 • 一方で、C++20 での定数式実行まわりの言語機能強化を踏まえ、リフレクション情報 を「値」として扱い、定数式(通常の関数)で処理する方向性を望む声も高まった。 • Reflection TS を初期案として、改めて C++20 の機能を踏まえた設計を始めた。 • 2023 年 10 月頃、値ベースのリフレクションの初稿(P2996)が完成。 • C++ コンパイラ(Clang, EDG)開発者による実験実装も公開された。 • 順調にいけば 2024 年秋頃には C++26 への採択が決まる見込み 165
リフレクション導入はどうなっている?
// P2996R4 列挙値を文字列に変換する例
template <typename E>
requires is_enum_v<E>
constexpr string enum_to_string(E value) {
template for (constexpr auto e : meta::enumerators_of(^E)) {
if (value == [:e:]) {
return string(meta::name_of(e));
}
}
return "<unnamed>";
}
enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");
166
42 最新の C++ を先取りする方法 167
最新の C++ を先取りする方法
{fmt} (https://fmt.dev/)は <format> のもとになったライブラリ。<format> や
<print> の C++26 相当の機能までを実装し、C++11 環境で動かすことができる。
#include <fmt/format.h>
#include <fmt/ranges.h>
int main() {
fmt::format("{}", vector{ 1, 2, 3 });
tuple<int, double, string> t { 1, 1.5, "a" };
fmt::println("{}", t);
}
#include <format>
#include <print>
int main() {
std::format("{}", vector{ 1, 2, 3 });
std::tuple<int, double, string> t { 1, 1.5, "a" };
std::println("{}", t);
}
168
最新の C++ を先取りする方法
range-v3 は C++20 以降の <ranges> や <algorithm> に含まれるレンジライブラリ
を先行実装したライブラリ。C++14 環境で使うことができる。
int main() {
std::vector<int> v{ 3, 2, 1 };
ranges::sort(v);
std::vector<int> vi{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto rng = vi | ranges::views::filter([](int i){ return i % 2 == 1; })
| ranges::views::transform([](int i){ return to_string(i); });
}
int main() {
std::vector<int> v{ 3, 2, 1 };
std::ranges::sort(v);
std::vector<int> vi{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto rng = vi | std::views::filter([](int i){ return i % 2 == 1; })
| std::views::transform([](int i){ return to_string(i); });
}
169
最新の C++ を先取りする方法
GSL (C++ Core Guideline Support Library) に含まれる gsl::span は、C++20 の
std::span の基になったライブラリ。
size_t Size(gsl::span<int> data) {
return data.size();
}
int main() {
int a[] = { 1,2,3 };
std::println("{}", Size(a));
}
size_t Size(std::span<int> data) {
return data.size();
}
int main() {
int a[] = { 1,2,3 };
std::println("{}", Size(a));
}
170
最新の C++ を先取りする方法
オンラインコンパイラ「Compiler Explorer」では、様々なコンパイラを選ぶことがで
き、C++ の新機能を試験実装しているブランチも含まれる。契約プログラミングの例:
double Div(double a, double b)
pre(b != 0.0)
// 事前条件
pre(std::isnan(a) == false) // 事前条件
pre(std::isnan(b) == false) // 事前条件
post(r: std::isfinite(r))
// 事後条件
{
contract_assert(std::isinf(a) == false); // アサーション
contract_assert(std::isinf(b) == false); // アサーション
return a / b;
}
int main() {
Div(10, 0);
}
171
43 ゲーム業界から C++ 標準化委員会への提言 172
ゲーム業界から C++ 標準化委員会への提言 • 2023 年、モントリオールのゲーム開発者らにより、C++ をゲーム開発でより使いや すい言語にしていくためのレポート(提案番号 P2966)が発表された。 【扱っているトピック】コンパイル時計算、動的メモリ確保を回避 したライブラリ機能、有用な属性、ムーブセマンティクスの改善、 例外機構の改善、パターンマッチング、C++ ツールの改善、ネット ワーク機能、並行プログラミング、ロギング及び I/O ライブラリ、 数値計算、その他 https://wg21.link/p2966 • レポート内では、こうしたゲーム業界による取り組みの周知や、レポートの内容につ いての業界内外からのフィードバックを呼びかけている。 • 「C++ はここがダメ」「C++ がこうなればいいのに」と思っているそこのあなた、 声を届けるチャンスです! 173
最新の C++ 情報にキャッチアップする方法 ◆ 新機能を知る • C++ 言語機能 | cpprefjp.github.io/lang.html • cppreference.com(英語) | en.cppreference.com/w/ ◆ 最新規格を扱っている本を探す • C++ 書籍 | cppmap.github.io/learn/books/ ◆ 提案文書を読む • 提案文書 | cppmap.github.io/standardization/proposals/ ◆ YouTube • C++ Weekly | @cppweekly 174
C++23 の機能を使える最新のコンパイラ ◆ コンパイラオプション • GCC 11- / Clang 13- : -std=c++2b • Visual Studio 2022 : /std:c++latest • Xcode : -std=c++2b ◆ コンパイラごとの、実装されている C++20 / 23 機能一覧表 en.cppreference.com/w/cpp/compiler_support 175
謝辞 ◆ 機能解説 監修 • 高橋 晶 さん (cpprefjp) • 川崎 洋平 さん (cpprefjp) ◆ Thanks • cpprefjp スポンサー・コントリビュータの皆様 • cppmap スポンサー・コントリビュータの皆様 (サンプルコードの一部は cpprefjp / cppmap の記事を引用しています) • 講演リハーサルに参加してくださった皆様 176
★ 質問 / 事前に寄せられた質問 多くの質問をいただきありがとうございました。 11 件をピックアップして回答スライドを用意しました。 177
質問 ① bitset(的な概念)の最初の 0(or 1)ビットの位置を高速に検索したり、n 番目の 0(or 1) ビットの位置を高速に検索したいのですが、標準ライブラリなどにこういった機能はあるので しょうか? 階層的な And / Or 演算でまとめた検索用のデータ構造を自作するしか無いのでしょ うか。 回答: 求められているものジャストのものはありませんが、各種のビット演算に関しては C++20 の <bit> にある関数が役に立つと思います。例えば、最初の 0 or 1 ビットの位置の検索は std::count~ 系の関数がそのまま使用できます。ただ、これらの関数を bitset で使用する場 合には .to_ullong() などで一度整数値に落としてやる必要があります。これは不便ですが、 C++26 に向けて bitset に各種ビット操作を行うメンバ関数を増やす提案(P3103R2)が検討 されています。また、<bit> に対してもビット置換系の関数を追加する提案(P3104R2)が C++26 に向けて検討されています。提案されている bit_compress 系の関数は、N 番目に立っ ているビット位置の検索の実装に役立ちそうです。 178
質問 ② enum class はキャストが必要になりがちで使いづらさがあります。「スコープ付きenum」「整数への暗 黙的な変換」「OR や AND なdの演算 (+代入も) の定義」のすべてを楽に実現する定番のテンプレートなど はありますか?(enable_if とか使うと 2 番目以外はできるがキャストが定義できない、自作クラスだと全部 実現できるが記述量が多くなる?)それともこの需要自体 enum class の存在理由と競合していたりします か? 回答: C++11 では、従来の enum もスコープ付き(Enum::xxx) で書けるようになりました。 メンバ型と して enum を定義すれば、整数への暗黙変換はそのままに、スコープだけ導入できます。この enum に型エ イリアスを宣言すると、scoped enum のように使えます。 C++11 より前は、非 enum class の列挙型名に対して、スコープ解決演算子(::)を使用できませんでし たが、C++11 でこれが可能になりました。(回答は次ページに続く→) enum Enum { Append, Truncate }; Enum e11 = Enum::Append; // C++11 から OK 179
質問 ②(回答続き) (→ 回答続き)ただし、この非 enum class の列挙型はスコープが無いため、このままだと名前空間に列挙 子名が露出します。そこで、構造体で包むことでスコープを付加します。 struct OpenMode_ { enum Enum { Append, Truncate }; }; OpenMode_::Enum e11 = OpenMode_::Enum::Append: // C++11 から OK さらに、このスコープを付加した列挙型に対してエイリアスを張ることで、名前空間に enum class を宣言 した場合の挙動を再現できます。これによって、非 enum class の列挙型(整数とのやり取りがスムーズ) に対してスコープだけを付加することができます。 struct OpenMode_ { enum Enum { Append, Truncate }; }; using OpenMode = OpenMode_::Enum; OpenMode e11 = OpenMode::Append: // C++11 から OK 180
質問 ③ ranges で view を複数組み合わせるような場合、for 文を生で書くのとどの程度パ フォーマンスの差があるのか気になります。 回答: 現行のコンパイラでは、単純なビューについては普通の for 文とほぼ同等になり ますが、複雑になると若干のオーバーヘッドがあるようです。コンパイラの進歩によっ てその差は小さくなると考えられます。C++23 以降の、複雑な処理ができる Range ア ダプタについては、自分で書くよりもコードが簡潔になりバグを生みにくいメリットが 勝ると思います。 181
質問 ④ string_view は NULL 終端である保証がないと思いますが、char* 型の引数を渡すよ うな API に対してのベストプラクティスはありますか。やはり一度 std::string にす るしかないんでしょうか…? 回答: 対応できる方法が標準には存在しないので、f(const char*) / f(const std::string&) でオーバーロードするしかないと思います。NULL 終端された文字列 専用の string_view ライクなクラスを自作するという方法もあります(GitHub でい くつか見つかります)。 182
質問 ⑤ std::vector の Allocator を改造すれば, スタック領域にデータを保持する型にでき ますか? 回答: はい、できます。ちなみに C++26 では、std::inplace_vector という、事前 に決めたサイズ内で、スタック領域上で動的にサイズ変更可能な vector が導入される 見込みです。 183
質問 ⑥ リフレクションの規格策定はどの段階まで進んでいるのでしょうか。また、現時点でリ フレクションを利用する場合は、どのような手段がデファクトスタンダードでしょうか。 回答: リフレクションの標準化は CWG での議論まで進んでいるため、C++26 に入る可 能性が高いです。一方、既存の C++ の範囲でリフレクションを実現している例として、 Qt では独自のコードジェネレーターにより、クラスの情報などをあらかじめコードに埋 め込む方法がとられています。 184
質問 ⑦ C++ のパッケージマネージャーについて、標準化委員会では何か話が進んだりしている のでしょうか? 回答: 標準的なパッケージマネージャーを用意するという話はありませんが、パッケー ジマネージャを含む、C++ 開発に関わる周辺ツール全般について、相互運用性を高める ために、共通の規格を整備しようとする議論が進んでいます。 185
質問 ⑧ std::vector ではなく std::array を使う利点とユースケースは何ですか? 回答: std::array は動的なメモリ確保を行いません。また、長さがコンパイル時に決 定します。ユースケースとしては、C 言語形式の配列を C++ コードに置き換える場合や、 コンパイル時に長さが確定する小さな配列について、動的なメモリ確保を行いたくない 場合などが考えられます。 186
質問 ⑨
auto x = const int{42}; に対して,
static_assert(std::same_as<decltype(x), const int>); は成立せず,
using T = const int; auto y = T{42}; に対して,
static_assert(std::same_as<decltype(y), const int>); も成立しません。
なぜですか?
回答: ここで x も y も型は int です。必要にならない限り auto で const が推論され
ることはありません。
const auto x = ...; であれば std::same_as<..., const int> が成立します。
187
質問 ⑩ 本スライド 6 番の「コンパイル時引数チェック」について、スライドの方法だと必ずコ ンパイル時定数が必要になります。実行時チェック版と共存する方法はありますか? constexpr コンストラクタにおいて、if constexpr (if consteval) と static_assert を使って同じことができますか? 回答: そのようなオーバーロードを書くと、常にコンパイル時チェックの無いほうが呼 ばれます(コンパイル時チェックのためのラッパ型は暗黙変換を用いていて、オーバー ロード解決で優先順が下がるため)。方法としては、C++26 で標準に入った std::runtime_format() での方法のように、コンパイル時チェックをバイパスする 関数やラッパ型を用いて引数を渡すのがおそらく簡単だと思います。2 つ目の質問につ いては、関数内で関数引数を定数式として使用できないため、やろうとするとエラーに なります。 188
質問 ⑪ 本スライド 13 番・14 番に関連して、.value_or() は引数に渡した式がすぐに評価さ れますが、モナディック関数ではそうではないのでしょうか? 回答: はい。std::optional のモナディック関数に渡すのは、値ではなく関数(関数 呼び出し可能なもの)であり、実際に必要とならない限り、渡した関数が呼び出される ことはありません。 189
更新履歴 2024-08-22 • 12 (p.61) 表内の a >=b | std::cmp_greater_equal(a, b) 欄の誤記を修正 2024-08-23 • 22 (p.94) C++20 を C++23 に修正 2024-08-29 • C++11 の機能紹介 (p.4) の重複を修正 2024-10-13 • 質疑応答スライドを追加、その他細かな修正 2024-10-14 • 質疑応答スライドを更新 2024-10-15 • 34 (p.136) サンプルコード内の i < を i <= に修正 190