第11章:モダンC++
はじめに
C++11は「モダンC++」の始まりとされ、言語に根本的な変革をもたらしました。Bjarne Stroustrupは次のように述べています:
> "C++11 feels like a new language." > > (C++11は新しい言語のように感じる)
本章では、C++11以降の主要な機能について学び、現代的なC++プログラミングスタイルを身につけます。
---
1. C++の進化
1.1 バージョン履歴
| バージョン | 年 | 主な追加機能 | |-----------|-----|-------------| | C++98/03 | 1998/2003 | 最初の標準、STL | | C++11 | 2011 | auto, ラムダ, ムーブ, スマートポインタ | | C++14 | 2014 | ジェネリックラムダ, 二進リテラル | | C++17 | 2017 | if constexpr, 構造化束縛, optional | | C++20 | 2020 | コンセプト, コルーチン, モジュール | | C++23 | 2023 | std::expected, std::print |
1.2 42課題との関係
42のCPP Moduleは主にC++98を対象としていますが、モダンC++の概念を理解することは重要です:
- C++98で学ぶ「なぜ」が、C++11以降の「どのように簡単になったか」を理解する基盤
- 実務ではC++11以降が標準
- 概念の理解が深まる
---
2. 型推論
2.1 auto
変数の型をコンパイラが推論:
// C++98
std::vector<std::string>::iterator it = vec.begin();
std::map<std::string, std::vector<int>>::const_iterator map_it = myMap.begin();
// C++11
auto it = vec.begin();
auto map_it = myMap.cbegin();
// 基本的な使用
auto x = 42; // int
auto y = 3.14; // double
auto z = "Hello"; // const char*
auto s = std::string("Hello"); // std::string
// 参照とconst
int value = 42;
auto a = value; // int (コピー)
auto& b = value; // int& (参照)
const auto& c = value; // const int& (const参照)
auto* p = &value; // int*
2.2 decltype
式の型を取得:
int x = 42;
decltype(x) y = 100; // int
// 関数の戻り値型に使用
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// decltype(auto) (C++14)
int& getRef();
decltype(auto) r = getRef(); // int& (参照を保持)
auto r2 = getRef(); // int (コピー)
2.3 型推論のガイドライン
// 良い使用例
auto iter = container.begin(); // イテレータ型は冗長
auto callback = [](int x) { return x * 2; }; // ラムダの型は書けない
auto result = someFunction(); // 戻り値型が明らか
// 避けるべき使用例
auto x = 42; // 明示的にint x = 42;の方が読みやすい場合も
auto data = getData(); // 戻り値型が不明確だと混乱する
---
3. 範囲forループ
3.1 基本構文
std::vector<int> vec = {1, 2, 3, 4, 5};
// C++98
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
// C++11
for (int x : vec) {
std::cout << x << std::endl;
}
// 参照を使用(変更可能)
for (int& x : vec) {
x *= 2;
}
// const参照(読み取り専用、コピー回避)
for (const int& x : vec) {
std::cout << x << std::endl;
}
// auto を使用
for (auto& x : vec) {
x *= 2;
}
for (const auto& x : vec) {
std::cout << x << std::endl;
}
3.2 様々なコンテナで使用
// 配列
int arr[] = {1, 2, 3, 4, 5};
for (int x : arr) {
std::cout << x << std::endl;
}
// std::map
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& [name, age] : ages) { // C++17 構造化束縛
std::cout << name << ": " << age << std::endl;
}
// C++11/14での書き方
for (const auto& pair : ages) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 初期化リスト
for (int x : {1, 2, 3, 4, 5}) {
std::cout << x << std::endl;
}
---
4. ラムダ式
4.1 基本構文
// 構文: [キャプチャ](引数) -> 戻り値型 { 本体 }
// 基本的なラムダ
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 5) << std::endl; // 8
// 戻り値型を明示
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
// 引数なしラムダ
auto greet = []() { std::cout << "Hello!" << std::endl; };
greet();
// 即時実行
[]() {
std::cout << "Immediately executed!" << std::endl;
}();
4.2 キャプチャ
int x = 10;
int y = 20;
// 値でキャプチャ(コピー)
auto by_value = [x, y]() {
std::cout << x << ", " << y << std::endl;
// x = 100; // エラー:デフォルトではconst
};
// 参照でキャプチャ
auto by_ref = [&x, &y]() {
x = 100;
y = 200;
};
by_ref();
std::cout << x << ", " << y << std::endl; // 100, 200
// 全てを値でキャプチャ
auto all_by_value = [=]() {
std::cout << x << ", " << y << std::endl;
};
// 全てを参照でキャプチャ
auto all_by_ref = [&]() {
x = 1000;
};
// ミックス
auto mixed = [=, &x]() { // yは値、xは参照
x = 42;
// y = 42; // エラー
};
// mutableラムダ(値キャプチャした変数を変更可能)
auto mutable_lambda = [x]() mutable {
x = 100; // ローカルコピーを変更
return x;
};
4.3 ジェネリックラムダ(C++14)
// autoを引数に使用
auto generic = [](auto a, auto b) {
return a + b;
};
std::cout << generic(1, 2) << std::endl; // 3 (int)
std::cout << generic(1.5, 2.5) << std::endl; // 4.0 (double)
std::cout << generic(std::string("Hello"), std::string(" World")) << std::endl;
// 完全転送
auto forward_lambda = [](auto&&... args) {
return someFunction(std::forward<decltype(args)>(args)...);
};
4.4 アルゴリズムとの組み合わせ
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
// ソート
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // 降順
});
// フィルタリング
auto it = std::remove_if(vec.begin(), vec.end(), [](int x) {
return x < 5;
});
vec.erase(it, vec.end());
// 変換
std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) {
return x * 2;
});
// 集約
int sum = std::accumulate(vec.begin(), vec.end(), 0, [](int acc, int x) {
return acc + x;
});
---
5. ムーブセマンティクス
5.1 問題:不要なコピー
// C++98: vectorを返すと大量のコピーが発生
std::vector<int> createLargeVector() {
std::vector<int> vec(1000000);
// ... データを設定
return vec; // コピーが発生(最適化がない場合)
}
std::vector<int> v = createLargeVector(); // さらにコピー
5.2 右辺値参照
// 左辺値(lvalue): 名前があり、アドレスが取れる
int x = 42;
int& lref = x; // 左辺値参照
// 右辺値(rvalue): 一時的な値
// int& error = 42; // エラー:左辺値参照は右辺値にバインドできない
int&& rref = 42; // 右辺値参照:OK
// std::move: 左辺値を右辺値にキャスト
std::string s = "Hello";
std::string&& rvalue = std::move(s);
// sはまだ有効だが、中身は不定
5.3 ムーブコンストラクタとムーブ代入演算子
class Buffer {
char* data;
size_t size;
public:
// コンストラクタ
Buffer(size_t n) : data(new char[n]), size(n) {}
// コピーコンストラクタ
Buffer(const Buffer& other) : data(new char[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
std::cout << "Copy constructor" << std::endl;
}
// ムーブコンストラクタ
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "Move constructor" << std::endl;
}
// コピー代入演算子
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size];
std::copy(other.data, other.data + size, data);
}
std::cout << "Copy assignment" << std::endl;
return *this;
}
// ムーブ代入演算子
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
std::cout << "Move assignment" << std::endl;
return *this;
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2 = b1; // コピーコンストラクタ
Buffer b3 = std::move(b1); // ムーブコンストラクタ
// b1は使用不可(有効だが不定状態)
Buffer b4(100);
b4 = b2; // コピー代入
b4 = std::move(b2); // ムーブ代入
return 0;
}
5.4 Rule of Five
C++11では、Rule of Threeが拡張されてRule of Fiveになりました:
class ModernResource {
public:
ModernResource() = default;
~ModernResource() = default;
// コピー
ModernResource(const ModernResource&) = default;
ModernResource& operator=(const ModernResource&) = default;
// ムーブ
ModernResource(ModernResource&&) noexcept = default;
ModernResource& operator=(ModernResource&&) noexcept = default;
};
---
6. スマートポインタ
6.1 unique_ptr
所有権が唯一のスマートポインタ:
#include <memory>
// 作成
std::unique_ptr<int> p1(new int(42));
auto p2 = std::make_unique<int>(42); // C++14、推奨
// 使用
std::cout << *p2 << std::endl;
// コピー不可
// std::unique_ptr<int> p3 = p2; // エラー
// ムーブは可能
std::unique_ptr<int> p3 = std::move(p2);
// p2はnullptrになる
// カスタムデリータ
auto fileDeleter = [](FILE* f) { fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> file(
fopen("test.txt", "r"),
fileDeleter
);
// 配列
auto arr = std::make_unique<int[]>(10);
arr[0] = 42;
6.2 shared_ptr
参照カウント方式の共有スマートポインタ:
#include <memory>
// 作成
auto p1 = std::make_shared<int>(42); // 推奨
std::cout << "Count: " << p1.use_count() << std::endl; // 1
{
std::shared_ptr<int> p2 = p1; // コピー可能
std::cout << "Count: " << p1.use_count() << std::endl; // 2
} // p2のデストラクタで参照カウント減少
std::cout << "Count: " << p1.use_count() << std::endl; // 1
// カスタムデリータ
auto custom_deleter = [](int* p) {
std::cout << "Deleting " << *p << std::endl;
delete p;
};
std::shared_ptr<int> p3(new int(100), custom_deleter);
6.3 weak_ptr
循環参照を防ぐための弱参照:
#include <memory>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 循環参照を防ぐ
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // weak_ptrなので循環参照にならない
// weak_ptrの使用
if (auto locked = node2->prev.lock()) { // shared_ptrに変換
// lockedを使用
}
return 0;
} // 両方のNodeが正しく破棄される
6.4 スマートポインタのベストプラクティス
// 良い例:make_sharedを使用
auto p = std::make_shared<MyClass>(args);
// 避けるべき例:newを直接使用
std::shared_ptr<MyClass> p(new MyClass(args));
// 関数に渡す場合
void useResource(const std::shared_ptr<Resource>& r); // 所有権を共有
void takeOwnership(std::unique_ptr<Resource> r); // 所有権を移動
void justUse(Resource& r); // 生ポインタ/参照で十分な場合
---
7. その他の重要な機能
7.1 nullptr
// C++98: NULLは0に展開される可能性
void func(int x);
void func(char* p);
func(NULL); // 曖昧になる可能性
// C++11: nullptrは明確にポインタ型
func(nullptr); // 常にfunc(char*)が呼ばれる
int* p = nullptr;
if (p == nullptr) {
// ...
}
7.2 constexpr
コンパイル時定数/関数:
// コンパイル時定数
constexpr int MAX_SIZE = 100;
// コンパイル時関数
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// コンパイル時に計算
constexpr int fact5 = factorial(5); // 120
int arr[factorial(5)]; // 配列サイズとして使用可能
// C++14以降はより複雑な関数も可能
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
7.3 static_assert
コンパイル時アサート:
static_assert(sizeof(int) == 4, "This code requires 32-bit integers");
static_assert(std::is_integral<int>::value, "int must be integral");
template <typename T>
class Container {
static_assert(std::is_default_constructible<T>::value,
"T must be default constructible");
// ...
};
7.4 初期化リスト
#include <initializer_list>
class MyArray {
std::vector<int> data;
public:
MyArray(std::initializer_list<int> list) : data(list) {}
};
MyArray arr = {1, 2, 3, 4, 5};
// 統一初期化構文
int x{42};
std::vector<int> vec{1, 2, 3, 4, 5};
std::map<std::string, int> ages{{"Alice", 30}, {"Bob", 25}};
7.5 可変長テンプレート引数
// 可変長引数
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 fold式
}
print(1, 2, 3); // 123
print("Hello", " ", "World"); // Hello World
// パラメータパックの展開
template <typename T, typename... Rest>
void printAll(T first, Rest... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
printAll(rest...);
}
}
7.6 構造化束縛(C++17)
// ペアの分解
std::pair<int, std::string> getPair() {
return {42, "Hello"};
}
auto [number, text] = getPair();
// mapのイテレーション
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << std::endl;
}
// 配列の分解
int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;
7.7 std::optional(C++17)
#include <optional>
std::optional<int> findValue(const std::vector<int>& vec, int target) {
auto it = std::find(vec.begin(), vec.end(), target);
if (it != vec.end()) {
return *it;
}
return std::nullopt;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
if (auto result = findValue(vec, 3)) {
std::cout << "Found: " << *result << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
// value_or: 値がなければデフォルト値を返す
int value = findValue(vec, 10).value_or(-1);
return 0;
}
---
まとめ
本章で学んだこと:
次章では、C++のベストプラクティスについて学びます。
---
練習問題
std::vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i * i);
}
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
std::cout << *it << std::endl;
}
- unique_ptrを使って、ファイルを安全に開閉するクラスを作成してください。
- 以下のクラスにムーブコンストラクタとムーブ代入演算子を追加してください:
class String {
char* data;
size_t length;
public:
String(const char* s);
String(const String& other);
String& operator=(const String& other);
~String();
};
- ラムダ式を使って、以下の操作を行うプログラムを作成してください: