C++/構文の基礎
C++の構文の基礎
[編集]C++の歴史と標準化
[編集]C++の誕生と歴史
[編集]C++はC言語の延長線上にある、マルチパラダイムプログラミング言語です。1979年にBjarne Stroustrupによって開発が開始され、1983年には「C with Classes」という名称で公開されました。1985年に正式にC++と命名され、産業界で広く利用されるようになりました。
C++は手続き型、オブジェクト指向、ジェネリック、メタプログラミングなど、さまざまなプログラミングスタイルをサポートしています。低レベルなシステムプログラミングから、ゲームやグラフィックスなどの高レベルなアプリケーションまで、幅広い分野で利用されています。
C++標準の策定経緯
[編集]1998年にC++の最初の標準規格であるC++98が発行されました。その後、以下のように規格が改訂されてきました。
- C++03 : 2003年に小さな修正が加えられた規格
- C++11 : 1998年以来の大きな改訂で、多くの新機能が追加された
- C++14 : C++11の小さな拡張
- C++17 : 並列プログラミングやプログラミングモデルの改良が行われた
- C++20 : モジュール、コンセプト、範囲for文の拡張、コルーチンなどの新機能が導入された
最新の規格はC++20ですが、現在はC++23の策定作業が進行中であり、さらなる機能強化や改善が期待されています。
C++11の概要
[編集]C++11は、C++98からかなり大規模な改訂が行われた規格です。主な新機能は以下の通りです。
- ラムダ式 : 無名関数を簡潔に記述できる
- auto : 型の自動推論が可能
- 範囲ベースfor文 : 範囲から要素を順に取り出せる
- nullptr : ヌルポインタを表す専用キーワード
- 新しいコンテナ : array, unordered_map, unordered_setなど
- スマートポインタ : unique_ptr, shared_ptr, weak_ptrなど
- 移動セマンティクス : リソースの所有権を効率よく移動できる
- 一貫した初期化構文 : 中括弧({ })で初期化できる
- 関数オブジェクト : std::functionで関数ポインタを置き換え可能
C++11以降、標準ライブラリが大幅に強化されただけでなく、言語機能自体も多数追加されています。
C++プログラムの構造
[編集]ソースファイル
[編集]C++のプログラムは1つ以上のソースファイル(.ccや.Cなど)で構成されます。ソースファイルにはプログラムのコードが記述されています。通常、メインとなる関数やクラスは個別のソースファイルに分割されます。
- main.cc
#include "myclass.h" auto main() -> int { MyClass obj; obj.doSomething(); return 0; }
プリプロセッサ
[編集]C++のソースファイルには、プリプロセッサディレクティブと呼ばれる特別な指令が書かれています。これらはヘッダーをインクルードしたり、マクロを定義したりするのに使われます。
#include <iostream> // iostreamヘッダをインクルード #define PI 3.14159 // PIという名前でマクロを定義
main関数
[編集]C++プログラムのエントリーポイント(開始点)はmain
関数です。main
関数から制御が開始され、プログラムが実行されていきます。
auto main() -> int { // 処理を記述 return 0; // 通常は0を返してプログラムを終了する }
main
関数は整数型を返す必要があり、返り値は正常終了時は0、エラー時は0以外の値を返します。
名前空間
[編集]名前空間は、同じ名前の関数やクラスなどが衝突するのを防ぐための機能です。たとえば、std名前空間はC++の標準ライブラリの機能を含みます。
#include <iostream> int value = 10; // グローバル変数 auto main() -> int { int value = 20; // ローカル変数 std::cout << "Global value: " << ::value << std::endl; // 10 std::cout << "Local value: " << value << std::endl; // 20 return 0; }
この例では、::value
とするとグローバル変数のvalue
が参照され、そうでない場合はローカル変数のvalue
が参照されます。
コメント
[編集]C++にはコードの説明を記述するためのコメント機能があります。
// 単一行コメント /* 複数行に渡る コメント */ // 注釈つきのコード int x = 5; // xに5を代入
コメントは実行時には無視されます。
データ型
[編集]基本データ型
[編集]C++には以下の基本データ型があります。
- 真理値型
bool
- 文字型
char
,wchar_t
,char8_t
,char16_t
,char32_t
- 整数型
short
,int
,long
,long long
- 浮動小数点型
float
,double
,long double
整数型と文字型にはさらにその型の修飾子として、signed
(符号つき)、unsigned
(符号なし)があります。
bool flag = true; char x = 'A'; int num = 42; double pi = 3.14159;
リテラル
[編集]リテラルとは、プログラム中に直接記述された定数値のことです。
42 // 整数リテラル 3.14 // 浮動小数点リテラル 'A' // 文字リテラル "Hello" // 文字列リテラル
ユーザーリテラル
[編集]C++11から追加されたユーザーリテラルを使うと、カスタムのリテラル接尾辞を定義できます。
#include <cstddef> #include <exception> #include <iostream> #include <stdexcept> #include <string> // ユーザーリテラルの定義 auto operator""_gb(const char *s) -> unsigned long long { std::string const str{s}; try { size_t pos = 0; unsigned long long const result = std::stoull(str, &pos); if (pos < str.size()) { throw std::invalid_argument("stringToULL: invalid argument"); } return result; } catch (const std::exception &e) { std::cerr << e.what() << std::endl; throw std::invalid_argument("stringToULL: invalid argument"); } } auto main() -> int { unsigned long long const data = 10_gb; // 10ギガバイトのデータサイズ std::cout << data << std::endl; return 0; }
この例では、_gb
というリテラル接尾辞が定義されています。これを使うと、10_gb
のように単位をリテラルに組み込むことができます。
型の変換
[編集]C++ではデータ型の異なる値を代入する際に、自動的に型変換が行われる場合があります。このとき、値が失われたり予期せぬ動作になる可能性があります。
int x = 5; // xは整数型5 float y = x; // yは浮動小数点型5.0 (安全な暗黙の型変換) int sum = x + y; // sumは10 (yは一時的に整数型に変換される)
上の例では、xからyへの代入は暗黙の型変換で安全に行われますが、sumの計算ではyが一時的に整数型に変換されるので、少数部分が失われます。
このような問題を防ぐため、明示的な型キャストを行うことができます。
int x = 5, y = 2; float div = static_cast<float>(x) / y; // div = 2.5
この例では、xを明示的にfloat
型にキャストしているので、割り算の結果が正しく浮動小数点数になります。C++には様々な型キャストがあり、それぞれ異なる動作をします。
static_cast
- コンパイル時に検査されるキャスト。基本的なキャスト
const_cast
const
の付加/除去に使うreinterpret_cast
- 低レベルの再解釈に使う(危険)
dynamic_cast
- 多態的なキャストで、安全性チェックが行われる
適切なキャストを使うことで、意図しない型変換を防ぐことができます。
オートデータ型(C++11)
[編集]C++11からはオートデータ型(auto
)を使って、右辺値の型から左辺値の型を推論することができます。
auto value = 3.14; // <code>double</code>と推論される auto flag = true; // <code>bool</code>と推論される
auto
を使うと、冗長な型指定を省略でき、可読性の向上につながります。特に複雑なジェネリックコードでは便利です。
演算子
[編集]C++には多くの演算子がありますが、ここでは主要な演算子について説明します。
算術演算子
[編集]+
(加算)、-
(減算)、*
(乗算)、/
(除算)、%
(剰余)の演算子を利用できます。
int x = 10 + 2; // 12 int y = 10 - 5; // 5 double z = 10.0 / 3; // 3.33333... int a = 11 % 3; // 2 (剰余)
ビット演算子
[編集]ビット単位の論理演算を行うビット演算子があります。
int x = 0b1010; // 2進数リテラル(C++14) int y = ~x; // y = 0b0101 (ビット否定) int z = x & 0b1100; // z = 0b1000 (ビット積) int w = x | 0b0011; // w = 0b1011 (ビット和)
代入演算子
[編集]=
は右辺の値を左辺に代入する代入演算子です。複合代入演算子も便利に使えます。
int x = 10; x += 5; // x = x + 5; と同じ(x = 15) x /= 3; // x = x / 3; と同じ(x = 5)
インクリメント/デクリメント演算子
[編集]++
はインクリメント演算子、--
はデクリメントです。前置/後置による違いに注意が必要です。
int x = 10; int y = x++; // x = 11, y = 10 (後置) int z = ++x; // x = 12, z = 12 (前置)
比較演算子
[編集]値の大小を比較する<
(未満)、>
(より大きい)、<=
(以下)、>=
(以上)などの演算子があります。
int x = 5, y = 10; bool res = (x < y); // res = true
論理演算子
[編集]&&
(論理積)、||
(論理和)、!
(論理否定)の論理演算子があり、条件分岐の制御などによく使われます。
bool res = (5 > 3) && (10 != 5); // res = true bool flag = !(5 == 5); // flag = false
条件演算子
[編集]?:
は三項演算子とも呼ばれる条件演算子で、条件式の値に応じて値を返します。
int max = x > y ? x : y; // xとyの大きい方をmaxに代入
制御構文
[編集]プログラムの実行フローを制御するための構文があります。
if文
[編集]if
文は条件によって実行するブロックを分岐できます。else
節、else if
節も利用できます。
int x = 10; if (x > 5) { // xは5より大きい } else if (x < 0) { // xは0未満 } else { // xは0以上5以下 }
switch文
[編集]switch
文は、整数値やenum
型に対する多岐に渡る分岐処理に使えます。
int value = 2; switch (value) { case 1: // value が 1 の場合 break; case 2: // value が 2 の場合 break; default: // どのcaseにも当てはまらない場合 break; }
default
節はどのケースにも当てはまらなかった場合に実行されます。break
を入れ忘れるとフォールスルーが発生しますので注意が必要です。
for文
[編集]for
文は反復処理に使う構文です。通常のカウンタ変数を使った記述の他、範囲for文を使うこともできます。
// 通常のfor文 for (int i = 0; i < 5; i++) { // 処理を繰り返す } // 範囲for文 (C++11) std::vector<int> values = {1, 2, 3, 4, 5}; for (int value : values) { // valuesの各要素に対する処理 }
範囲for文では、コンテナや配列の各要素を順に参照できます。
while文とdo-while文
[編集]while
文とdo-while
文は条件式の真偽値で反復処理を制御します。
int count = 0; while (count < 5) { // 条件を先に評価 // 処理を繰り返す count++; } do { // 最初は無条件で処理を1回実行する // 処理を繰り返す count++; } while (count < 5); // 条件を後に評価
while
文は条件式を先に評価し、do-while
文は処理を1回実行した後に条件式を評価するという違いがあります。
break、continue
[編集]break
文はループから抜け出す際に使い、continue
文は次のループ反復に進む際に使います。
for (int i = 0; i < 10; i++) { if (i == 5) { break; // iが5の場合ループを抜ける } if (i % 2 == 0) { continue; // 偶数の場合は処理をスキップ } // 奇数の場合のみここが実行される }
ラムダ式 (C++11)
[編集]C++11から追加されたラムダ式を使うと、匿名関数オブジェクトを簡潔に記述できます。
#include <algorithm> #include <iostream> #include <vector> auto main() -> int { auto values = std::vector{2, 3, 5, 7, 11}; // 2倍にする処理をラムダ式で記述 std::transform(values.begin(), values.end(), values.begin(), [](int x) { return x * 2; }); // 値を出力 for (int const value : values) { std::cout << value << " "; // 4 6 10 14 22 と出力される } std::cout << std::endl; return 0; }
ラムダ式は無名関数オブジェクトを生成し、その場で関数を定義して渡せるので、関数ポインタよりも柔軟で expressvie なコードが書けます。
関数
[編集]C++ではプログラムを関数の集合として構造化できます。
関数の定義と呼び出し
[編集]関数は戻り値の型 関数名(引数の型 引数名, ...)
のように定義します。
// 2つの整数の大きい方を返す関数 int getMax(int x, int y) { return x > y ? x : y; } auto main() -> int { int a = 3, b = 7; int maxValue = getMax(a, b); // maxValueは7 return 0; }
この例では、getMax
関数が定義され、main
関数から呼び出されています。
スコープルール
[編集]C++ではスコープルールに従って、変数や関数の有効範囲が決まります。
int globalVar = 10; // グローバルスコープ int functionScope() { int x = 20; // 関数スコープ if (true) { int y = 30; // ブロックスコープ // xとyはここで有効 } // yはここでは無効 return x; }
ブロックの外側で宣言された変数は、そのブロック内で参照できますが、逆は許されません。
デフォルト引数
[編集]関数の引数にはデフォルト値を設定できます。この機能を使うと、引数の一部または全部を省略して関数を呼び出せます。
int getValue(int x, int y = 20, int z = 30) { return x + y + z; } auto main() -> int { int res1 = getValue(5); // x=5, y=20, z=30 より res=55 int res2 = getValue(1, 2); // x=1, y=2, z=30 より res=33 return 0; }
デフォルト引数は右から順に設定しなければなりません。
参照渡し
[編集]通常の引数渡しは値渡しですが、参照渡しも可能です。参照渡しを行うと、関数内での値の変更が外でも有効になります。
void swap(int& x, int& y) { int temp = x; x = y; y = temp; } auto main() -> int { int a = 1, b = 2; swap(a, b); // aとbの値が入れ替わる return 0; }
参照渡しは、大きなデータ構造を渡す際に便利ですが、側効果に注意が必要です。
オーバーロード
[編集]C++では同じ関数名で引数の型や個数が異なる関数(オーバーロード関数)を定義できます。
auto add(int x, int y) -> int { return x + y; } auto add(double x, double y) -> double { return x + y; } auto main() -> int { int const sum1 = add(1, 2); // 整数版のaddが呼ばれる double const sum2 = add(1.2, 3.4); // 浮動小数点数版のaddが呼ばれる return 0; }
この機能を使うと、同じ概念の操作を同じ名前で表現できるので、プログラムの可読性が向上します。
関数ポインタ
[編集]C++では関数もポインタとして扱えます。関数ポインタを使うと、実行時に関数を動的に切り替えられるメリットがあります。
auto add(int x, int y) -> int { return x + y; } auto operation(int a, int b, int (*func)(int, int)) -> int { return func(a, b); // funcで指定された関数を実行する } auto main() -> int { int const res = operation(3, 4, add); // resは7になる return 0; }
関数ポインタは、コールバック関数を渡すときなどに役立ちますが、可読性が悪くなる場合もあります。C++11以降ではラムダ式を使うことでこの問題が緩和されます。
関数オブジェクト (C++11)
[編集]C++11ではstd::function
を使って関数オブジェクトを扱えるようになりました。これにより、関数ポインタよりも型安全で表現力の高いコードが書けるようになりました。
#include <functional> auto add(int x, int y) -> int { return x + y; } auto main() -> int { auto const ops = std::function{add}; int const res = ops(3, 4); // resは7 return 0; }
std::function
オブジェクトには関数ポインタだけでなく、ラムダ式やバインドされた関数オブジェクトなども代入できます。
配列
[編集]配列は複数の同じ型の要素を格納するデータ構造です。
配列の基本
[編集]配列は型 配列名[要素数]
のように定義します。
int data[5]; // 要素数5の配列 data[0] = 10; // 最初の要素に10を代入 data[4] = 50; // 最後の要素に50を代入 int arr[] = {1, 2, 3, 4, 5}; // 初期化子リストから要素数が決まる
配列の添字(インデックス)は0から始まり、arr[0]
が先頭要素、arr[4]
が最後尾要素になります。初期化子リストを使うと、要素数を明示する必要がありません。
配列名そのものはその配列の先頭要素を指すポインタのように振る舞います。
int ary[] = {1, 2, 3}; int* ptr = ary; // aryは&arr[0]を表す
多次元配列
[編集]C++では多次元配列も使えます。多次元配列は、要素が配列であるような配列です。
int matrix[2][3] = { {1, 2, 3}, {4, 5, 6} }; matrix[0][1] = 10; // matrix[0]の2番目の要素に10を代入
この例ではmatrix
は2x3の2次元配列です。多次元配列は、メモリ上では単一の配列として確保されることに注意が必要です。
可変長配列 (C++11)
[編集]C++11からは、可変長配列(VLA: Variable Length Array
)がサポートされました。可変長配列は、実行時に決まるサイズで配列を確保できます。
int size = 10; double data[size]; // サイズ10の配列を確保
ただし、可変長配列には制限もあり、グローバル変数やstatic
変数では使えず、クラスのメンバー変数としても使えません。また、一部の環境ではサポートされていない可能性があります。
ポインタ
[編集]ポインタはメモリ上のデータの位置を表す値です。C++ではポインタを広範に使います。
ポインタの基礎
[編集]ポインタ変数は*
演算子を使って宣言します。&
演算子は変数のアドレスを取得します。
int x = 5; int* ptr = &x; // ptrにxのアドレスを代入 *ptr = 10; // xの値が10になる int y = *ptr; // yは10になる
nullptr
はC++11で追加された、ヌルポインタを表す安全なリテラルです。
ポインタと配列
[編集]配列名はその配列の先頭要素のアドレスを指すポインタとして振る舞います。
int data[] = {1, 2, 3, 4, 5}; int* ptr = data; // dataはdata[0]のアドレスを指す for (int i = 0; i < 5; i++) { std::cout << ptr[i] << " "; // ptrを配列のように使用 std::cout << *(ptr + i) << " "; // 別の記述方法 }
これにより、配列要素へのアクセスにポインタの算術演算を利用できます。
ポインタの算術演算
[編集]ポインタに対して算術演算を行うと、そのポインタの指す型のサイズ分をバイト単位で移動します。
int data[] = {1, 2, 3}; int* ptr = data; // ptrはdata[0]を指す ptr++; // ptrはdata[1]を指す ptr += 2; // ptrはdata[3]を指す(存在しない領域)
この性質を利用して、動的メモリ確保した領域を順にアクセスしていくことができます。
ダイナミックメモリ管理
[編集]C++ではヒープ領域からメモリを動的に確保・解放できます。
int* data = new int[5]; // 整数型の配列を動的に確保 data[0] = 10; // 配列の使用 // ... delete[] data; // 確保したメモリを解放
new
演算子でメモリを確保し、delete
演算子で解放します。メモリリークを避けるため、確保したメモリは必ず解放する必要があります。
スマートポインタ (C++11)
[編集]C++11で導入されたスマートポインタを使うと、メモリリークのリスクを軽減できます。
#include <memory> auto main() -> int { std::unique_ptr<int> ptr(new int(10)); // int型を動的に確保 // ... return 0; // ptrの破棄時に自動で解放される }
unique_ptr
はスコープを出ると自動的にdelete
が呼ばれるので、手動での解放が不要になります。shared_ptr
などのスマートポインタも利用できます。
構造体と共用体
[編集]構造体と共用体を使うと、異なる型のデータを1つにグループ化できます。
構造体の定義
[編集]struct
キーワードを使って構造体を定義します。
struct Point { int x; int y; }; auto main() -> int { Point p1; // {x=0, y=0} p1.x = 2; p1.y = 3; Point p2 = {5, 7}; // {x=5, y=7} return 0; }
構造体のメンバーにはドット(.
)を使ってアクセスします。構造体の初期化も可能です。
ビットフィールド
[編集]構造体のメンバーにビットフィールドを使うと、メモリ利用効率が上がります。
struct Flags { unsigned int flag1 : 1; // 1ビット unsigned int flag2 : 2; // 2ビット unsigned int flag3 : 3; // 3ビット // ... };
ビットフィールドメンバーには、利用するビット数を指定します。
共用体
[編集]共用体(union
)では、メンバーが共有のメモリ領域を使い、1つのメンバーしか有効にできません。
union Value { int x; double y; }; auto main() -> int { Value v; v.x = 5; // vには整数値が入る v.y = 3.14; // vには浮動小数点数が入る(xは上書きされる) return 0; }
共用体はメモリを節約したい場合に便利ですが、安全性には注意が必要です。
無名共用体 (C++11)
[編集]struct Data { int x; union { // 無名共用体 int y; double z; }; }; auto main() -> int { Data d; d.x = 1; d.y = 2; // yかzのどちらか一方にしかアクセスできない return 0; }
無名共用体は構造体のメンバーとして振る舞いますが、メンバー自体には名前がありません。無名共用体のメンバーにはドット(.)を使ってアクセスします。
無名共用体は匿名ユニオンとも呼ばれ、構造体のメモリレイアウトを最適化したり、互換性のない機能をまとめたりするのに役立ちます。
クラス
[編集]クラスはオブジェクト指向プログラミングにおける中心的な概念です。
クラスの定義
[編集]class
キーワードを使ってクラスを定義します。
class Rectangle { private: int width, height; public: Rectangle(int w, int h) : width{w}, height{h} {} auto getArea() const -> int const { return width * height; } };
このRectangle
クラスには、width
とheight
という2つのメンバー変数と、コンストラクタとgetter
メソッドが定義されています。
コンストラクタ/デストラクタ
[編集]コンストラクタはオブジェクトの生成時に呼び出される特殊なメンバー関数です。デストラクタはオブジェクトの破棄時に呼び出されます。
class Example { public: Example() { /* コンストラクタの処理 */ } ~Example() { /* デストラクタの処理 */ } };
コンストラクタ/デストラクタを使って、オブジェクトのライフサイクルに合わせたリソース管理が可能になります。
メンバー関数
[編集]メンバー関数はクラスのオブジェクトに対して操作を行う関数です。
class Counter { private: int count{0}; public: Counter() = default; void increment() { count++; } auto getCount() const -> int const { return count; } };
オブジェクトに対してメンバー関数を呼び出すには、ドット(.)を使います。
auto main() -> int { Counter c; c.increment(); // countが1になる std::cout << c.getCount(); // 1が出力される return 0; }
アクセス指定子
[編集]public
、private
、protected
というアクセス指定子を使って、メンバーへのアクセス権を制御できます。
public
- どこからでもアクセス可能
private
- そのクラスの内部からのみアクセス可能
protected
- そのクラスとその派生クラスの内部からアクセス可能
一般に、メンバー変数はprivate
、メンバー関数はpublic
として設計されます。
フレンド関数/クラス
[編集]friend
宣言を使うと、クラス外の関数またはクラスに対して、クラスのプライベートメンバーへのアクセス権を付与できます。
class Truck; // 前方宣言が必要 class Engine { friend class Truck; // Truckクラスをフレンドに設定 int horsepower; public: explicit Engine(int hp) : horsepower{hp} {} }; class Truck { Engine eng; public: explicit Truck(int hp) : eng{hp} {} [[nodiscard]] auto getHorsePower() const -> int const { return eng.horsepower; } // エンジンの馬力を取得 };
この例では、Engine
クラスはTruck
クラスをフレンドとして設定しています。これにより、Truck
クラスはEngine
クラスのプライベートメンバーであるhorsepower
にアクセスできます。
演算子のオーバーロード
[編集]C++ではユーザ定義の型に対して、演算子をオーバーロードすることができます。演算子オーバーロードにより、既存の演算子に新しい意味を持たせることができます。
class Vector2D { double x, y; public: Vector2D(double _x, double _y) : x{_x}, y{_y} {} auto operator+(const Vector2D& other) const -> Vector2D { return {x + other.x, y + other.y}; } }; auto main() -> int { Vector2D const v1{1.0, 2.0}; Vector2D const v2{3.0, 4.0}; Vector2D const v3 = v1 + v2; // v3は(4.0, 6.0)となる return 0; }
この例では、+
演算子をオーバーロードして、Vector2D
クラスのインスタンス同士の加算を定義しています。
継承
[編集]継承を使うと、既存のクラスを拡張して新しいクラスを作ることができます。
#include <iostream> #include <string> #include <utility> class Animal { protected: std::string name; public: explicit Animal(std::string n) : name{std::move(n)} {} void setName(const std::string& n) { name = n; } virtual void makeSound() = 0; // 純粋仮想関数 }; class Dog : public Animal { public: explicit Dog(const std::string& n) : Animal{n} {} void makeSound() override { std::cout << "Woof!" << std::endl; } };
この例では、Dog
クラスがAnimal
クラスを公開継承しています。Animal
クラスのmakeSound
関数は純粋仮想関数として宣言されており、派生クラスでオーバーライドされる必要があります。
継承によりコードの再利用性が高まりますが、過剰な継承は設計を複雑にする可能性があるので注意が必要です。
テンプレート
[編集]テンプレートを使うと、型パラメータを持つジェネリックなコードを書くことができます。
関数テンプレート
[編集]関数テンプレートは、あらゆる型に対して働く関数を定義できます。
template <typename T> auto max(T a, T b) -> T { return a > b ? a : b; } auto main() -> int { int const x = max(3, 7); // int版のmaxが呼ばれる double const y = max(2.5, 9.8); // double版のmaxが呼ばれる return 0; }
この例では、max
関数が関数テンプレートとして定義されています。max
関数は、テンプレート引数T
の型に基づいたバージョンがコンパイル時に自動的に生成されます。
クラステンプレート
[編集]同様にして、クラスもテンプレート化することができます。
#include <numbers> #include <vector> template <typename T> class Stack { private: std::vector<T> data; public: void push(T value) { data.push_back(value); } auto pop() -> T { T value = data.back(); data.pop_back(); return value; } }; auto main() -> int { Stack<int> intStack; intStack.push(3); intStack.push(7); Stack<double> doubleStack; doubleStack.push(3.14); doubleStack.push(std::numbers::e); return 0; }
この例では、Stack
クラスがテンプレート化されています。テンプレートにより、Stack
クラスは任意の型T
の要素を格納できるようになります。main
関数内で、int
型とdouble
型のStack
オブジェクトが作成されています。
テンプレートの特殊化
[編集]特定の型に対してテンプレートの振る舞いを変更したい場合、特殊化を行うことができます。
// 標準の実装 template <typename T> class Vector { public: Vector(int n, const T& value) { /* ... */ } // ... }; // int型の場合の特殊化 template <> class Vector<int> { public: Vector(int n) { /* int型の場合の特別な実装 */ } // ... };
この例では、Vector
クラステンプレートのint
型に対する特殊化が行われています。Vector<int>
の場合のみ、コンストラクタの引数が異なる特別な実装が行われます。
部分特殊化
[編集]クラステンプレートに対しては部分特殊化も可能です。これにより、いくつかのテンプレート引数に対してのみ振る舞いを変更できます。
// 標準の実装 template <typename T, typename U> class Pair { public: Pair(const T& first, const U& second) { /* ... */ } // ... }; // ポインタ型の場合の部分特殊化 template <typename T, typename U> class Pair<T*, U*> { public: Pair(T* first, U* second) { /* ポインタの場合の特別な実装 */ } // ... };
この例では、Pair
クラステンプレートのテンプレート引数が両方ともポインタ型の場合に、部分特殊化が行われています。
部分特殊化は柔軟性が高い反面、誤った実装をしやすいので注意が必要です。
テンプレートメタプログラミング
[編集]テンプレートは実行時だけでなく、コンパイル時にも様々な計算を行えます。これをテンプレートメタプログラミングと呼びます。
template <int N> struct Factorial { static const int value = N * Factorial<N - 1>::value; }; template <> struct Factorial<0> { static const int value = 1; }; auto main() -> int { constexpr int n = 5; constexpr int fact = Factorial<n>::value; // fact = 120 return 0; }
この例では、再帰的にファクトリアルを計算するためのクラステンプレートが定義されています。Factorial
クラステンプレートのvalue
は、コンパイル時の定数として計算されます。
このようなメタプログラミングは、コンパイル時に値や型に関する様々な処理を行うことができるため、柔軟でパフォーマンスの高いジェネリックコードを書くことができます。
名前空間
[編集]名前空間は、グローバルスコープの名前の衝突を防ぐためのメカニズムです。
名前空間の基本
[編集]namespace
キーワードを使って名前空間を定義します。名前空間内に関数やクラスなどを入れることができます。
namespace MyNamespace { int value = 10; void print(int x) { std::cout << x << std::endl; } } auto main() -> int { MyNamespace::print(MyNamespace::value); // 10が出力される return 0; }
名前空間に所属する名前は、スコープ解決演算子(::
)を使ってアクセスします。
名前空間の分割
[編集]名前空間は複数のソースファイルに分割して定義することができます。
- file1.cc
namespace MyNamespace { int value = 10; }
- file2.cc
namespace MyNamespace { void print(int x) { std::cout << x << std::endl; } } auto main() -> int { MyNamespace::print(MyNamespace::value); // 10が出力される return 0; }
これにより、名前空間の定義を複数のファイルに分散できるので、プロジェクトの規模が大きくなった時に便利です。
無名名前空間
[編集]名前が付けられていない無名の名前空間も使えます。無名名前空間は内部リンケージを持つため、その名前空間に所属する名前は同一の翻訳単位内でのみ可視となります。
- file1.cc
namespace { int value = 10; // この値は他のファイルから見えない } auto main() -> int { // value++; // エラー return 0; }
- file2.cc
namespace { int value = 20; // file1.ccとは別の値 }
無名名前空間は特定のソースファイル内部でのみ可視な、名前の衝突を防ぐ手段として用いられます。
名前空間のエイリアス (C++11)
[編集]C++11から、名前空間に別名をつけることができるようになりました。
namespace LongName { // ... } namespace ShortName = LongName; auto main() -> int { ShortName::someFunction(); // LongName::someFunctionと同じ return 0; }
名前空間のエイリアスを使うと、長い名前を短い別名で参照できるので便利です。
例外処理
[編集]例外処理は、プログラムの実行時にエラー状況が発生した際の対処機構です。
try、catch、throw
[編集]例外の送出にはthrow
を、例外の捕捉にはtry
とcatch
を使います。
#include <exception> #include <iostream> #include <stdexcept> auto divideNumber(double a, double b) -> double { if (b == 0) { throw std::runtime_error("Division by zero!"); } return a / b; } auto main() -> int { try { double const result = divideNumber(10.0, 0.0); std::cout << result << std::endl; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } return 0; }
この例では、divideNumber
関数内で0による除算が行われると例外std::runtime_error
が送出されます。main
関数のtry
ブロック内でその例外が捕捉され、catch
ブロックで適切な処理が行われます。
throw
式の後には任意の型の値を指定でき、catch
節でその型を指定して例外を捕捉することができます。
例外階層
[編集]C++には標準でstd::exception
を根本とする例外クラスの階層が用意されています。
std::exception
+-- std::logic_error
| +-- std::domain_error
| +-- std::invalid_argument
| +-- std::length_error
| +-- std::out_of_range
+-- std::runtime_error
+-- std::overflow_error
+-- std::underflow_error
+-- std::range_error
特定の種類の例外を捕捉したい場合は、適切な例外クラスのcatch
節を記述します。
try { // 処理 } catch (const std::logic_error& e) { // 論理的な例外を捕捉 } catch (const std::runtime_error& e) { // 実行時の例外を捕捉 }
catch
節は上から順に評価されるので、一般的な例外から特殊な例外の順に記述する必要があります。
例外安全性
[編集]例外安全性とは、例外が発生した際にオブジェクトやリソースがどの程度安全な状態に残るかを表す概念です。一般に、以下の3つのレベルがあります。
- 基本的な例外安全性
- データ構造がおかしくなる可能性はあるが、リソースリークは起こさない
- 強い例外安全性
- 例外が発生してもデータ構造やリソースが変更される前の状態に残る
- ノースロー保証
- 例外が発生しない(そもそも例外を送出しない)
適切な例外処理を行い、例外安全性を確保することが重要です。C++11のスマートポインタなどの機能を活用することも有効な手段です。
その他の機能
[編集]プリプロセッサマクロ
[編集]プリプロセッサマクロは、コンパイル前にソースコードに対して文字置換などの処理を行う機能です。#define
で定義されます。
#define MAX(a, b) ((a) > (b) ? (a) : (b)) int x = MAX(5, 10); // xは10になる
この例では、MAX(a, b)
がコンパイル前に((a) > (b) ? (a) : (b))
に置換されます。マクロは単純な置換処理しか行えませんが、コンパイル時の最適化などに使われます。
インラインアセンブラ
[編集]C++ではインラインアセンブラを使ってアセンブリ言語を直接記述できます(環境依存)。
#include <iostream> auto main() -> int { int value = 0; // インラインアセンブラでxレジスタに10を設定する asm ("movl $10, %eax" : "=a"(value) // xレジスタの値をvalueに出力 ); std::cout << value << std::endl; // 10が出力される return 0; }
インラインアセンブラは低レベルの最適化などの目的で使われますが、ポータビリティが失われるデメリットがあります。
アトリビュート (C++11)
[編集]C++11では、クラス、関数、変数などにアトリビュートを指定できるようになりました。アトリビュートは、プログラムに関する追加的な情報を伝えるために使われます。
[[nodiscard]] int someFunction() { /* ... */ } auto main() -> int { someFunction(); // 警告が出る return 0; }
この例では、nodiscard
アトリビュートによって、someFunction
の戻り値が無視されると警告が出力されます。これにより、不注意なミスを防ぐことができます。
アトリビュートは将来的に拡張される可能性があり、C++20ではコンセプトなどの新機能でアトリビュートが使われています。
可変引数テンプレート (C++11)
[編集]C++11から、可変数の引数をテンプレートパラメータパックとして受け取れるようになりました。
#include <iostream> template <typename T, typename... Args> void print(T first, Args... args) { std::cout << first << std::endl; if constexpr (sizeof...(args) > 0) { print(args...); } } auto main() -> int { print(10, "hello", 3.14, true); // 出力: 10 // hello // 3.14 // 1 return 0; }
このprint
関数は、最初の引数と残りの可変長引数を別々に受け取ることができます。sizeof...
は引数パックの要素数を返します。パターン分割を使うと、再帰的に残りの引数を処理することができます。
可変引数テンプレートは、テンプレートメタプログラミングにおいて重要な役割を果たします。
これらは、C++における主要な機能の一部を紹介しました。言語・標準ライブラリの機能は年々拡張されており、最新の規格を確認することが重要です。また、実務でC++を使う際は、コーディング規約やベストプラクティスに従うことも大切です。