●Nullable型とは?
皆さんは、C++でプログラミングをする際に、nullポインターによるエラーに悩まされたことはありませんか?
nullポインターは、初心者からベテランエンジニアまで、誰もが一度は遭遇するやっかいな問題ですよね。
でも、安心してください。
C++には、そんなnullポインターによるエラーを防ぐための強力な武器があるんです。
それが、Nullable型です。
○Nullable型の概要
Nullable型とは、その名の通り、nullを許容する型のことを指します。
通常の型とは異なり、値の有無を表現することができるんですね。
Nullable型を使えば、変数がnullを含む可能性があることを明示的に表すことができます。
これにより、nullポインターによるエラーを未然に防ぐことができるわけです。
○Nullable型を使うメリット
Nullable型を使うことで、nullポインターによるエラーを防ぐことができます。
Nullable型を使えば、変数がnullを含む可能性があることを明示的に示せるので、うっかりnullポインターを参照してしまうようなミスを防げるんですね。
また、コードの可読性も向上します。
Nullable型を使えば、変数がnullを含む可能性があることが一目でわかるので、コードを読む人も、その変数の性質を理解しやすくなります。
○Nullable型を使わない場合の問題点
逆に、Nullable型を使わない場合はどうでしょうか。
まず、nullポインターによるエラーが発生するリスクが高くなります。
変数がnullを含む可能性があるかどうかがわからないので、うっかりnullポインターを参照してしまうことがあるんですね。
また、コードの可読性も下がります。変数がnullを含む可能性があるかどうかがわからないので、コードを読む人は、その変数の性質を推測しながら読まなければならなくなるんです。
実際、私も過去にnullポインターによるエラーに悩まされたことがあります。
ただ、Nullable型を使うようになってからは、そういったエラーはほとんど発生しなくなりました。
●std::optionalを使ったNullable型の実装
さて、前回はNullable型の概要とそのメリットについて解説しましたが、今回は実際にC++でNullable型を実装する方法について見ていきましょう。
C++でNullable型を実装する方法は多くありますが、ここでは最も一般的な方法である、std::optionalを使った方法を紹介します。
○std::optionalの基本的な使い方
std::optionalは、C++17から標準ライブラリに追加された、Nullable型を実装するためのクラスです。
std::optionalを使えば、簡単にNullable型を実装できます。
std::optionalの基本的な使い方は、次のようになります。
std::optionalは、テンプレート引数で指定した型のNullable変数を宣言できます。
上記の例では、int型のNullable変数を宣言しています。
value1は、42で初期化されているので、値を持っている状態になります。
一方、value2は初期値を与えていないので、値を持っていない状態になります。
○サンプルコード1:std::optionalを使った変数の宣言と初期化
では、実際にサンプルコードを見てみましょう。
下記のコードは、std::optionalを使ってint型のNullable変数を宣言し、初期化する例です。
実行結果
value1は、42で初期化されているので、value()メンバ関数で値を取得できます。
一方、value2は初期値を与えていないので、value()メンバ関数を呼び出すとエラーになります。
そのため、value_or()メンバ関数を使って、値を持っていない場合のデフォルト値を指定しています。
○サンプルコード2:std::optionalの値の有無のチェック
次に、std::optionalが値を持っているかどうかをチェックする方法を見てみましょう。
下記のコードは、std::optionalが値を持っているかどうかをチェックし、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。
実行結果
has_value()メンバ関数を使えば、std::optionalが値を持っているかどうかをチェックできます。
値を持っている場合はtrue、持っていない場合はfalseを返します。
○サンプルコード3:std::optionalの値の取得
最後に、std::optionalから値を取得する方法を見てみましょう。
下記のコードは、std::optionalから値を取得し、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。
実行結果
value_or()メンバ関数を使えば、std::optionalから値を取得できます。
std::optionalが値を持っている場合は、その値を返します。
持っていない場合は、引数で指定したデフォルト値を返します。
●std::unique_ptrを使ったNullable型の実装
前回は、std::optionalを使ってNullable型を実装する方法を紹介しましたが、今回はもう一つの方法として、std::unique_ptrを使ったNullable型の実装方法について解説します。
std::unique_ptrは、C++11から導入されたスマートポインターの一種で、動的に割り当てられたリソースの所有権を管理するために使用されます。
std::unique_ptrを使えば、生ポインターを直接扱うことなく、Nullable型を実装できます。
○std::unique_ptrの基本的な使い方
std::unique_ptrの基本的な使い方は、次のようになります。
std::unique_ptrは、テンプレート引数で指定した型のスマートポインターを作成できます。
上記の例では、int型のスマートポインターを作成しています。
ptr1は、std::make_unique()関数を使って42で初期化されているので、値を持っている状態になります。
一方、ptr2は初期値を与えていないので、値を持っていない状態になります。
○サンプルコード4:std::unique_ptrを使ったNullableなポインターの宣言
それでは実際に、std::unique_ptrを使ってNullableなポインターを宣言するサンプルコードを見てみましょう。
実行結果
ptr1は、std::make_unique()関数で初期化されているので、値を持っています。
そのため、if文の条件式でtrueとなり、ptr1の値が出力されます。
一方、ptr2は初期値を与えていないので、値を持っていません。
そのため、if文の条件式でfalseとなり、”ptr2 has no value”というメッセージが出力されます。
○サンプルコード5:std::unique_ptrの値の有無のチェック
次に、std::unique_ptrが値を持っているかどうかをチェックする方法を見てみましょう。
下記のコードは、std::unique_ptrが値を持っているかどうかをチェックし、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。
実行結果
std::unique_ptrが値を持っているかどうかは、if文の条件式で直接チェックできます。
値を持っている場合はtrue、持っていない場合はfalseを返します。
○サンプルコード6:std::unique_ptrの値の取得
最後に、std::unique_ptrから値を取得する方法を見てみましょう。
下記のコードは、std::unique_ptrから値を取得し、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。
実行結果は以下のようになります。
std::unique_ptrから値を取得するには、ポインターのデリファレンス演算子(*)を使用します。
ただし、std::unique_ptrが値を持っていない場合にデリファレンス演算子を使用すると、未定義動作となってしまいます。
そのため、上記のコードでは、三項演算子を使って、std::unique_ptrが値を持っている場合はその値を、持っていない場合はデフォルト値を返すようにしています。
●Nullable型を使ったクラス設計
さて、ここまででstd::optionalとstd::unique_ptrを使ったNullable型の実装方法について解説しましたが、実際のプログラミングでは、Nullable型をクラスのメンバ変数として使用したり、メソッドの返り値として使用したりすることが多いですよね。
そこで今回は、Nullable型を使ったクラス設計について解説していきます。
Nullable型をうまく活用することで、より安全で効率的なコードを書くことができるようになりますよ。
○Nullable型をメンバ変数に持つクラスの設計
まずは、Nullable型をメンバ変数に持つクラスの設計について考えてみましょう。
例えば、人物を表すPersonクラスを設計する際に、年齢を表すage変数をNullable型にすることで、年齢が不明な人物を表現できます。
○サンプルコード7:Nullable型をメンバ変数に持つクラスの実装例
下記のコードは、std::optionalを使ってage変数をNullable型にしたPersonクラスの例です。
実行結果
Personクラスのコンストラクタでは、名前を表すm_name変数と、年齢を表すm_age変数を受け取ります。
m_age変数はstd::optional型なので、年齢が不明な場合はstd::nulloptを渡すことができます。
getAge()メソッドでは、m_age変数の値を返します。
m_age変数がstd::nulloptの場合は、値を持っていないことを表します。
main()関数では、年齢が20歳のAliceと、年齢が不明なBobの2つのPersonオブジェクトを作成し、それぞれの名前と年齢を出力しています。
Bobの年齢はstd::nulloptなので、”unknown”と出力されます。
○Nullable型を返り値に持つメソッドの設計
次に、Nullable型を返り値に持つメソッドの設計について考えてみましょう。
例えば、リストからある要素を検索するメソッドを設計する際に、Nullable型を返り値にすることで、検索に失敗した場合に特別な値を返すことができます。
○サンプルコード8:Nullable型を返り値に持つメソッドの実装例
下記のコードは、std::optionalを返り値に持つ、リストから要素を検索するメソッドの例です。
実行結果
findElement()関数は、std::vector型のリストと、検索する値を受け取り、見つかった要素をstd::optional型で返します。
要素が見つからない場合は、std::nulloptを返します。
main()関数では、3と6を検索しています。3は見つかるので、”Found element: 3″と出力されます。
一方、6は見つからないので、”Element not found”と出力されます。
●Nullable型を使う際の注意点
これまで、Nullable型の実装方法やクラス設計における活用方法について解説してきましたが、実際にNullable型を使う際には、いくつか注意点があります。
ここでは、そんなNullable型を使う際の注意点について、具体的なサンプルコードを交えながら解説していきますので、ぜひ参考にしてみてくださいね。
○Nullable型の変数の初期化
Nullable型の変数を宣言する際には、初期値を明示的に指定することが重要です。
初期値を指定しないと、変数の状態が不定になってしまい、予期せぬバグの原因になることがあります。
例えば、下記のようなコードは、コンパイルは通りますが、望ましくありません。
この場合、valueがどのような状態にあるのかがわかりません。
値を持っているのか、それともstd::nulloptなのかが不明確です。
そのため、Nullable型の変数を宣言する際には、次のように初期値を明示的に指定するようにしましょう。
このように初期値を明示的に指定することで、変数の状態が明確になり、バグを防ぐことができます。
○Nullable型の変数のデフォルト値
Nullable型の変数は、値を持っていない状態を表現できるため、デフォルト値を指定する必要がない場合があります。
例えば、次のような関数を考えてみましょう。
この関数は、Personオブジェクトが年齢を持っている場合はその値を、持っていない場合はstd::nulloptを返します。
○Nullable型の変数の比較方法
Nullable型の変数を比較する際には、少し注意が必要です。
コードを見てみましょう。
○サンプルコード9:Nullable型の変数の比較方法
実行結果
value1とvalue2は、どちらも42を持っているので、等しいと判定されます。
一方、value1は42を持っているのに対し、value3はstd::nulloptなので、等しくないと判定されます。
●Nullable型の応用例
これまでは、Nullable型の基本的な使い方や注意点について解説してきましたが、ここでは、Nullable型のより実践的な応用例を紹介していきます。
Nullable型は、様々な場面で活用できます。
例えば、キャッシュの実装、遅延初期化、オプション引数の処理などに使えます。
ここでは、そんなNullable型の応用例を、具体的なサンプルコードを交えて解説していきますので、ぜひ参考にしてみてくださいね。
○サンプルコード10:Nullable型を使ったキャッシュの実装
まずは、Nullable型を使ったキャッシュの実装例を見てみましょう。
キャッシュは、計算コストの高い処理の結果を保存しておくことで、同じ計算を繰り返すことを避けるためのテクニックです。
下記のコードは、フィボナッチ数列の計算結果をキャッシュするためにNullable型を使った例です。
実行結果
このコードでは、std::vectorを使ってキャッシュ用の配列cacheを定義しています。
cacheの要素はstd::optional型なので、計算結果が保存されていない場合はstd::nulloptになります。
fibonacci()関数では、まずcache[n]に計算結果が保存されているかをチェックします。
保存されていれば、その値を返します。保存されていない場合は、再帰的にフィボナッチ数列を計算し、その結果をcache[n]に保存してから返します。
このように、Nullable型を使えば、キャッシュの実装がシンプルになります。
キャッシュに値が保存されているかどうかを、Nullable型の値の有無で判定できるからです。
○サンプルコード11:Nullable型を使った遅延初期化の実装
次に、Nullable型を使った遅延初期化の実装例を見てみましょう。
遅延初期化とは、変数の初期化を、実際に変数が使用されるまで遅らせるテクニックです。
これで、不要な初期化処理を避けることができます。
下記のコードは、Nullable型を使って遅延初期化を実装した例です。
実行結果
このコードでは、WidgetManagerクラスのm_widget変数をstd::optional型にすることで、Widgetオブジェクトの初期化を遅延させています。
doSomethingWithWidget()メソッドが呼ばれると、まずm_widgetが値を持っているかをチェックします。
値を持っていない場合は、新しいWidgetオブジェクトを作成して、m_widgetに保存します。
そして、m_widget->doSomething()を呼び出します。
このように、Nullable型を使えば、遅延初期化を簡単に実装できます。
必要になるまで初期化を遅らせることで、無駄な初期化処理を避けられます。
○サンプルコード12:Nullable型を使ったオプション引数の実装
最後に、Nullable型を使ったオプション引数の実装例を見てみましょう。
オプション引数とは、省略可能な引数のことです。
Nullable型を使えば、オプション引数を簡単に実装できます。
下記のコードは、Nullable型を使ってオプション引数を実装した例です。
実行結果
このコードでは、greet()関数の第2引数messageをstd::optional型にすることで、オプション引数にしています。messageに値が渡された場合は、その値を出力します。
渡されなかった場合は、何も出力しません。
このように、Nullable型を使えば、オプション引数を簡単に実装できます。
省略可能な引数をNullable型にすることで、引数の有無を簡単に判定できるようになります。
まとめ
皆さん、ここまでお読みいただき、ありがとうございました
本記事では、C++でのNullable型の概要と利用方法について、詳しく解説してきました。
Nullable型を使う際には、変数の初期化やデフォルト値、比較方法などに注意が必要ですが、適切に使いこなすことで、C++プログラミングの幅が広がります。
皆さんのC++プログラミングの参考になれば幸いです。