コンテンツにスキップ

C++ の基礎 Day 7

講義のコードに登場する C++ の機能やテクニックの補足。

1. #pragma once(プラグマ・ワンス)

説明
  • 同じヘッダファイルを複数回インクルードしても、1 回だけインクルードされるようにするためのプリプロセッサディレクティブ。
#pragma once

// ヘッダファイルの内容
  • マクロによるインクルードガード(下記)よりも簡潔。
#ifndef SECCAMP_COMMON_HPP
#define SECCAMP_COMMON_HPP

// ヘッダファイルの内容

#endif
  • 標準の仕様には含まれないが、現代ではほぼすべてのコンパイラでサポートされているため、多くのプロジェクトで使用される。

まとめ

  • インクルードガードを簡潔に書ける。

2. 一手間加えたマクロ

説明
  • コンパイル時フラグとして、マクロの 0 / 1 を使う手法はよく見られるが、次のような欠点がある。

#include <print>
#define I_LOVE_SECCAMP 1
#define I_LOVE_SECHACK 0

// --- ここまでライブラリ内部 ---

int main()
{
#if I_LOVE_SECCAMP
	std::println("1.SecCamp!");
#endif

#if I_LOVE_SECHACK
	std::println("1.SecHack!");
#endif

// スペルミスがあるが、コンパイルエラーまたは警告として検出されない(悲しい)
#if I_LOVE_CAMP
	std::println("2.SecCamp!");
#endif

// スペルミスがあるが、コンパイルエラーまたは警告として検出されない(悲しい)
#if I_LOVE_HACK
	std::println("2.SecHack!");
#endif

// if と ifdef を取り違えても、気付きにくい(悲しい)
#ifdef I_LOVE_SECCAMP
	std::println("3.SecCamp!");
#endif

// if と ifdef を取り違えても、気付きにくい(悲しい)
#ifdef I_LOVE_SECHACK
	std::println("3.SecHack!");
#endif
}
出力
1.SecCamp!
3.SecCamp!
3.SecHack!

  • 関数形式のマクロを応用すると、利点が多い。
#include <print>

#define I_LOVE(X) I_LOVE_PRIVATE_DEFINITION_##X()
#define I_LOVE_PRIVATE_DEFINITION_SECCAMP()	0
#define I_LOVE_PRIVATE_DEFINITION_SECHACK()	0

#if 1
	#undef	I_LOVE_PRIVATE_DEFINITION_SECCAMP
	#define I_LOVE_PRIVATE_DEFINITION_SECCAMP()	1
#else
	#undef	I_LOVE_PRIVATE_DEFINITION_SECHACK
	#define I_LOVE_PRIVATE_DEFINITION_SECHACK()	1
#endif

// --- ここまでライブラリ内部 ---

int main()
{
// 関数形式のマクロなので、if が自然に見える(うれしい)
#if I_LOVE(SECCAMP)
	std::println("1.SecCamp!");
#endif

// 関数形式のマクロなので、if が自然に見える(うれしい)
#if I_LOVE(SECHACK)
	std::println("1.SecHack!");
#endif

// スペルミスがコンパイルエラーまたは警告として検出される(うれしい)
#if I_LOVE(CAMP)
	std::println("2.SecCamp!");
#endif

// スペルミスがコンパイルエラーまたは警告として検出される(うれしい)
#if I_LOVE(HACK)
	std::println("2.SecHack!");
#endif

// 関数形式のマクロなので、誤って ifdef を使った際の不自然さが強調される(うれしい)
// コンパイルエラーまたは警告としても検出される(うれしい)
#ifdef I_LOVE(SECCAMP)
	std::println("3.SecCamp!");
#endif

// 関数形式のマクロなので、誤って ifdef を使った際の不自然さが強調される(うれしい)
// コンパイルエラーまたは警告としても検出される(うれしい)
#ifdef I_LOVE(SECHACK)
	std::println("3.SecHack!");
#endif
}

まとめ

  • コンパイル時フラグの明確さと堅牢性が向上する。

3. [[nodiscard]](ノーディスカード)

説明
  • 関数に [[nodiscard]] 属性を付けると、その関数の戻り値を無視したときに警告が出るようになる。
[[nodiscard]]
int f()
{
	return 42;
}

int main()
{
	f(); // 警告が出る
}
  • 戻り値を無視することが論理的におかしい関数で使用すれば、戻り値を無視するバグを防ぐことができる。
  • すべての関数に付けると厄介になる(例えば printf など)ので、適切に使う。

まとめ

  • 関数の戻り値を無視してしまうミスを防ぐことができる。

4. constexpr(コンストエクスプレッション)

説明
  • 関数に constexpr を付けると、その計算をコンパイル時に評価できる場合はコンパイル時に評価されるようになる。
  • 関数の戻り値をコンパイル時定数の文脈で使うことができる。
#include <print>

constexpr int Square(int x)
{
	return x * x;
}

int main()
{
	int a[Square(3)] = {}; // コンパイル時に int a[9] と解釈される

	std::println("{}", std::size(a));

	// static_assert を使って、コンパイル時に評価されているかを確認可能
	static_assert(Square(3) == 9);
}
  • C++ の多くのアルゴリズム関数が constexpr 対応してきている。
#include <algorithm>
#include <vector>
#include <print>

constexpr int MinElement(std::vector<int> a)
{
	std::ranges::sort(a); // コンパイル時にソート

	return a[0];
}

int main()
{
	static_assert(MinElement({ 40, 20, 10, 5, 30 }) == 5);
}
  • コンパイル時計算は、実行時の処理を減らすことができるほか、エラーの発見を実行時からコンパイル時に早めることにも役立つ。

まとめ

  • 適切な関数に constexpr を付けることで、計算を実行時からコンパイル時に移すことができる。

5. 制約(標準コンセプト)

説明
  • 関数テンプレートの型パラメータに対して、特定の条件を満たす型のみを受け付けるようにするための「制約」という機能がある。
  • いくつかの制約は標準ライブラリに含まれており、これを「標準コンセプト」と呼ぶ。
  • std::integral(インテグラル)は int, long, short, char などの整数型のみを受け付ける標準コンセプト。
  • 制約により、テンプレートのインスタンス化におけるコンパイルエラーを、より早い時点で検出できる。
template <class Type> // どんな型でもインスタンス化しようとする
bool IsEven(Type n)
{
	return n % 2 == 0;
}

int main()
{
	IsEven(42); // OK
	IsEven(3.14); // コンパイルエラー。エラーの場所は n % 2 == 0 の行
}
  • std::integral を使うことで、コンパイルエラーが関数呼び出し時点で検出される。
#include <concepts>

bool IsEven(std::integral auto n) // 引数として整数型のみを受け付ける
{
	return n % 2 == 0;
}

int main()
{
	IsEven(42); // OK
	IsEven(3.14); // コンパイルエラー。適合する関数が見つからない。エラーの場所はこの行
}
  • 他にも std::floating_point, std::signed_integral, std::unsigned_integral などがある。

まとめ

  • 制約を使うことで、テンプレートの型に対する条件を明確にし、コンパイルエラーをより早い時点で検出できる。

6. noexcept(ノーエクセプト)

説明
  • 関数に noexcept を付けると、その関数が例外を投げないことを示す。
  • 例外を投げない関数は、コンパイラが最適化を行いやすくなる。
  • 標準ライブラリの一部の処理は、例外を投げないことが保証されていると効率化される。
  • 関連記事: noexcept - cpprefjp

まとめ

  • 例外を投げない関数に noexcept を付けることで、コンパイラによる最適化が促進される。

7. Hidden Friends(ヒデンフレンズ)

説明
  • ある関数が、本来無関係なクラスに対してオーバーロードの候補として挙がってしまうことを防げるテクニック。
  • 次のコードでは、int * Money というコードに対して、無関係な operator *(int, const Point&) も候補に挙がってしまう。

struct Point
{
	int x;
	int y;
};

Point operator *(int n, const Point& p)
{
	return Point{ n * p.x, n * p.y };
}

struct Money
{
	int dollars;
	int cents;
};

int main()
{
	Money money{ 1, 23 };

	// コンパイルエラー。operator *(int, const Point&) も候補に挙がる
	3 * money;
}
エラーメッセージの例
prog.cc: In function 'int main()':
prog.cc:23:11: error: no match for 'operator*' (operand types are 'int' and 'Money')
23 |         3 * money;
	|         ~ ^ ~~~~~
	|         |   |
	|         int Money
prog.cc:7:7: note: candidate: 'Point operator*(int, const Point&)'
	7 | Point operator *(int n, const Point& p)
	|       ^~~~~~~~
prog.cc:7:38: note:   no known conversion for argument 2 from 'Money' to 'const Point&'
	7 | Point operator *(int n, const Point& p)
	|                         ~~~~~~~~~~~~~^

  • Hidden Friends を使うと、operator *(int, const Point&)Money に対して候補に挙がらないようにできる。
  • これにより誤ったオーバーロード解決を防いだり、コンパイル時間を短縮したりできる。

struct Point
{
	int x;
	int y;

	friend Point operator *(int n, const Point& p)
	{
		return Point{ n * p.x, n * p.y };
	}
};

struct Money
{
	int dollars;
	int cents;
};

int main()
{
	Money money{ 1, 23 };

	// コンパイルエラー
	3 * money;
}
エラーメッセージの例
prog.cc: In function 'int main()':
prog.cc:23:11: error: no match for 'operator*' (operand types are 'int' and 'Money')
23 |         3 * money;
	|         ~ ^ ~~~~~
	|         |   |
	|         int Money

  • ここ数年注目されているテクニックで、C++20 以降では標準ライブラリでも活用されている。
  • 関連記事: Hidden Friends - cpprefjp

まとめ

  • Hidden Friends を使うことで、誤ったオーバーロード解決を防ぎ、コンパイル時間を短縮できる。

8. 比較演算子の default(デフォルト)実装

説明
  • クラスに ==!= 演算子を実装する際、各メンバについて単純に ==!= を適用するだけでよい場合、= default を使って実装を省略できる。
  • == 演算子を実装すれば、自動的に != 演算子も実装される。

#include <print>

struct Point
{
	int x;
	int y;

	friend bool operator ==(const Point& lhs, const Point& rhs) = default;
};

int main()
{
	Point p1{ 1, 2 };
	Point p2{ 1, 2 };

	if (p1 == p2)
	{
		std::println("p1 == p2");
	}

	if (p1 != p2)
	{
		std::println("p1 != p2");
	}
}
出力
p1 == p2

まとめ

  • クラスのオブジェクトの比較が単純な場合、==!= 演算子を = default で実装すると便利。

9. static(スタティック)メンバ関数

説明
  • クラスのインスタンスを作らずに呼び出せるメンバ関数。
  • クラス名で修飾して呼び出すので、クラスに関連するヘルパー関数、ユーティリティ関数をわかりやすくまとめられる。
  • クラスのメンバにアクセスするために this ポインタの受け渡しが必要になる非静的メンバ関数よりも、オーバーヘッドが少ない。
struct Point
{
	int x;
	int y;

	static Point Zero()
	{
		return{ 0, 0 };
	}
};

int main()
{
	Point p = Point::Zero();
}

まとめ

  • あるクラスに関連するユーティリティ関数を static メンバ関数でまとめると便利。

10. コンストラクタの explicit(エクスプリシット)指定

説明
  • コンストラクタに explicit が無い場合、意図しない暗黙的な型変換が行われることがある。
#include <print>

class User
{
public:

	User(int id)
		: m_id(id) {}

	int getID() const
	{
		return m_id;
	}

private:

	int m_id = 0;
};

void CompareUser(const User& user1, const User& user2)
{
	std::println("User1 ID: {}", user1.getID());
	std::println("User2 ID: {}", user2.getID());
}

int main()
{
	User user1{ 123 };

	CompareUser(user1, true); // true → User{ 1 } として扱われる
}
  • コンストラクタに explicit を付けると、暗黙的な型変換を禁止し、意図しない型変換を防ぐことができる。
#include <print>

class User
{
public:

	explicit User(int id)
		: m_id(id) {}

	int getID() const
	{
		return m_id;
	}

private:

	int m_id = 0;
};

void CompareUser(const User& user1, const User& user2)
{
	std::println("User1 ID: {}", user1.getID());
	std::println("User2 ID: {}", user2.getID());
}

int main()
{
	User user1{ 123 };

	//CompareUser(user1, true); // コンパイルエラー

	CompareUser(user1, User{ 456 }); // OK
}
  • 基本的には、引数 1 つで呼べるコンストラクタに explicit を付けるとよい。

まとめ

  • 引数 1 つで呼べるコンストラクタは、暗黙的な型変換による意図しない挙動を防ぐために explicit を付けるとよい。

11. pImpl(ピンプル)パターン

説明
  • pImpl(Pointer to Implementation)パターンは、クラスの実装を隠蔽するためのデザインパターン。
  • クラスの実装を別のクラスに隠蔽することで、ユーザに見せる側のクラスのインターフェースを安定させ、クラスの実装を変更してもユーザに影響を与えないようにできる。
  • ポインタ経由のアクセスになるため、パフォーマンスが少し低下するが、多くの場合ではメリットのほうが大きい。
User.hpp
#pragma once
#include <memory> // std::shared_ptr

class User
{
public:

	User();

	~User();

	int getID() const;

private:

	// ここには宣言だけあればよい。実装詳細は User.cpp で定義する
	class Impl;

	// 実装詳細へのポインタだけを持つ
	std::shared_ptr<Impl> m_pImpl;
};
User.cpp
#include <string>
#include <vector>
#include "User.hpp"

// 実装詳細
class User::Impl
{
public:

	Impl(int id)
		: m_id{ id } {}

	int getID() const
	{
		return m_id;
	}

private:

	// これら実装の詳細はユーザから隠蔽される
	std::vector<int> m_data1;
	std::string m_data2;
	int m_id = 0;
};

User::User()
	: m_pImpl{ std::make_shared<Impl>(0) } {}

User::~User() = default;

int User::getID() const
{
	return m_pImpl->getID();
}

まとめ

  • pImpl パターンを使うことで、クラスの実装を隠蔽し、ユーザに安定したインターフェースを提供できる。
  • ヘッダでインクルードするライブラリを減らすことができる。

12. 移譲コンストラクタ

説明
  • コンストラクタから別のコンストラクタを呼び出すことができる機能。
  • コンストラクタの初期化リストで : を使って、他のコンストラクタを呼び出す。
  • 同じパターンを繰り返すコードを減らすことができる。
#include <string>

struct User
{
	int id;

	std::string name;

	int age;

	User(int _id, const std::string& _name, int _age)
		: id{ _id }
		, name{ _name }
		, age{ _age } {}

	User(int _id, const std::string& _name)
		: User{ _id, _name, 0 } {} // 移譲コンストラクタ
};

int main()
{
	User user1{ 1, "Alice", 20 };

	User user2{ 2, "Bob" };
}

まとめ

  • 移譲コンストラクタを使うことで、コンストラクタのコードの重複を減らすことができる。

13. explicit operator bool()(エクスプリシット・オペレーター・ブール)

説明
  • クラスに explicit operator bool() を実装すると、bool 型として振る舞うことが期待される文脈(if 文の条件式など)において、そのクラスのオブジェクトが true または false として評価されるようになる。

#include <print>

struct User
{
	int id = 0;

	explicit operator bool() const
	{
		return id != 0; // id が 0 でない場合に true を返す
	}
};

int main()
{
	User user1{ 42 };

	if (user1) // User 型が bool 型のように振る舞う
	{
		std::println("user1 は有効なユーザです");
	}

	User user2;

	if (!user2) // User 型が bool 型のように振る舞う
	{
		std::println("user2 は無効なユーザです");
	}

	//bool b1 = user1; // これはコンパイルエラー。明示的なキャストが必要

	bool b2 = static_cast<bool>(user2); // OK
}
出力
user1 は有効なユーザです
user2 は無効なユーザです

まとめ

  • explicit operator bool() を実装することで、クラスのオブジェクトが適切な場面で bool 型のように振る舞えるようになり、if 文の条件式などで直感的に使える。

14. void*(ヴォイドポインタ)

説明
  • すべての型のポインタは void* または const void* にキャストすることができる。
#include <print>

void ShowAddress(const void* p)
{
	std::println("Address: {}", p);
}

struct Point
{
	int x;
	int y;
};

int main()
{
	int n = 42;
	double d = 3.14;
	Point p{ 1, 2 };

	ShowAddress(&n);
	ShowAddress(&d);
	ShowAddress(&p);
}

まとめ

  • さまざまな型のポインタを受け入れる関数を作る際は void* を使う。

15. requires(リクワイアーズ)

説明
  • 型の制約を記述するための構文。(5. 参照)
  • テンプレートの型パラメータに対して、特定の条件を満たす型のみを受け付けるよう制約を設けることができる。
#include <concepts>
#include <print>

// T は整数型または浮動小数点型でないといけない
template <class T>
	requires std::integral<T> || std::floating_point<T>
T Square(T x)
{
	return x * x;
}

int main()
{
	std::println("{}", Square(42));
	std::println("{}", Square(3.14));
	//std::println("{}", Square("Hello")); // コンパイルエラー
}

まとめ

  • テンプレートの型パラメータに対して、特定の条件を満たす型のみを受け付けるよう制約を設けることで、用途を明確にし、型パラメータのエラーを早期に検出できる。

16. std::addressof(アドレスオブ)

説明
  • (通常はそのようなことはしないが)& 演算子をオーバーロードしているクラスから、そのオブジェクトのアドレスを取得するための関数。
  • どのようなクラスに対しても、確実にアドレスを取得するために使う。
#include <memory>
#include <print>

struct Object
{
	int n = 0;

	// & 演算子を使うとなぜか n * n を返す
	int operator &()
	{
		return (n * n);
	}
};

int main()
{
	Object obj{ 5 };

	// & 演算子をオーバーロードしているため、アドレスを取得できない
	//Object* p = &obj;
	std::println("{}", &obj); // 25

	// std::addressof を使うと、オーバーロードされた & 演算子を回避してアドレスを取得できる
	Object* p = std::addressof(obj);

	std::println("{}", p->n);
}

まとめ

  • std::addressof を使うと、& 演算子をオーバーロードしているクラスからもアドレスを取得できる。

17. seccamp::Color クラスで見られるコンストラクタオーバーロード

説明
  • { } による初期化は縮小変換に対して厳密で、ときに面倒なことが起こる。
struct RGB
{
	unsigned char r;
	unsigned char g;
	unsigned char b;

	RGB() = default;;

	RGB(unsigned char _r, unsigned char _g, unsigned char _b)
		: r{ _r }
		, g{ _g }
		, b{ _b } {}
};

int main()
{
	for (int i = 0; i < 256; ++i)
	{
		// RGB color1{ i, i, i }; // int -> unsigned char への縮小変換が発生するためエラー

		RGB color2{ static_cast<unsigned char>(i), static_cast<unsigned char>(i), static_cast<unsigned char>(i) }; // OK

		RGB color3(i, i, i); // OK
	}
}
  • あらゆる初期化で { } をシンプルに使いたい場合は、厳しさを緩和したコンストラクタオーバーロードを用意できる。
#include <concepts>

struct RGB
{
	unsigned char r;
	unsigned char g;
	unsigned char b;

	RGB() = default;;

	RGB(unsigned char _r, unsigned char _g, unsigned char _b)
		: r{ _r }
		, g{ _g }
		, b{ _b } {}

	RGB(std::integral auto _r, std::integral auto _g, std::integral auto _b)
		: r{ static_cast<unsigned char>(_r) }
		, g{ static_cast<unsigned char>(_g) }
		, b{ static_cast<unsigned char>(_b) } {}
};


int main()
{
	for (int i = 0; i < 256; ++i)
	{
		RGB color1{ i, i, i }; // OK

		RGB color3(i, i, i); // OK
	}
}
  • 一方で縮小変換を見落とすことにもつながるのでプログラマの責任は増加する、うまくバランスを考える。

まとめ

  • { } による初期化で縮小変換の回避が面倒な場合、より寛容なコンストラクタオーバーロードを用意することができる。

18. クラスの前方宣言

説明
  • あるクラスのポインタや参照を使う一方、そのクラスのメンバに関する情報が必要ない場合、クラスの前方宣言だけで十分。
  • ヘッダーの相互参照の問題を解決するためにも使える。
User.hpp
#pragma once

class User
{
public:

	User(int id)
		: m_id{ id } {}

	int getID() const
	{
		return m_id;
	}

private:

	int m_id = 0;
};
Show.hpp
#pragma once

class User; // 前方宣言。User というクラスがあることだけを示す
// ここで User.hpp をインクルードする必要はない

void ShowUser(const User& user);
Show.cpp
#include <print>
#include "Show.hpp"
#include "User.hpp" // User の .getID() を使うためにインクルード

void ShowUser(const User& user)
{
	std::println("User ID: {}", user.getID());
}

まとめ

  • クラスの詳細な情報が必要ない場合、クラスの前方宣言だけで十分。コードをシンプルに保つことができる。

19. #pragma pack(プラグマ・パック)

説明
  • コンパイラに対して、クラスのメンバのアライメントを変更する指示を与える。
  • ファイルのヘッダなど、特殊な配置を必要とする構造体を扱う際に役立つ。
#include <cstdint>
#include <print>

struct Test
{
	std::uint16_t a;

	// 4 バイト境界にそろえるため、通常はここに 2 バイトのパディングが入る

	std::uint32_t b;

	std::uint16_t c;

	// 4 バイト境界にそろえるため、通常はここに 2 バイトのパディングが入る
};

int main()
{
	std::println("sizeof(Test): {}", sizeof(Test)); // sizeof(Test): 12
}
#include <cstdint>
#include <print>

#pragma pack(push, 2) // 2 バイト境界でパッキングを開始

struct Test
{
	std::uint16_t a;

	std::uint32_t b;

	std::uint16_t c;
};

// パッキングをデフォルトに戻す
#pragma pack(pop)

int main()
{
	std::println("sizeof(Test): {}", sizeof(Test)); // sizeof(Test): 8
}
  • 実行時性能に影響を与えるため、必要な場合にのみ使用する。

まとめ

  • パディングが生じないようメンバ変数を配置するために、#pragma pack を使うことができる。
  • ただし、実行時性能に影響を与えるため、必要な場合にのみ使用する。

20. static_assert(スタティックアサート)

説明
  • コンパイル時に条件をチェックし、条件が偽の場合はコンパイルエラーを発生させる。
  • コンパイル時に評価できない(定数式にならない)場合にもエラーが発生する。
int main()
{
	static_assert(sizeof(int) == 4);
}

まとめ

  • コンパイル時に結果がわかる式は static_assert でチェックするとよい。