Nullable型とは?C++での利用方法を実例12選で解説

C++のNullable型C++
この記事は約28分で読めます。

※本記事のコンテンツは、利用目的を問わずご活用いただけます。実務経験10000時間以上のエンジニアが監修しており、基礎知識があれば初心者にも理解していただけるように、常に解説内容のわかりやすさや記事の品質に注力しております。不具合・分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。(理解できない部分などの個別相談も無償で承っております)
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)


●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の基本的な使い方は、次のようになります。

#include <optional>

std::optional<int> value1 = 42;  // int型のNullable変数を宣言し、42で初期化
std::optional<int> value2;      // int型のNullable変数を宣言するが、初期値は与えない

std::optionalは、テンプレート引数で指定した型のNullable変数を宣言できます。

上記の例では、int型のNullable変数を宣言しています。

value1は、42で初期化されているので、値を持っている状態になります。

一方、value2は初期値を与えていないので、値を持っていない状態になります。

○サンプルコード1:std::optionalを使った変数の宣言と初期化

では、実際にサンプルコードを見てみましょう。

下記のコードは、std::optionalを使ってint型のNullable変数を宣言し、初期化する例です。

#include <iostream>
#include <optional>

int main() {
    std::optional<int> value1 = 42;
    std::optional<int> value2;

    std::cout << "value1: " << value1.value() << std::endl;
    std::cout << "value2: " << value2.value_or(0) << std::endl;
}

実行結果

value1: 42
value2: 0

value1は、42で初期化されているので、value()メンバ関数で値を取得できます。

一方、value2は初期値を与えていないので、value()メンバ関数を呼び出すとエラーになります。

そのため、value_or()メンバ関数を使って、値を持っていない場合のデフォルト値を指定しています。

○サンプルコード2:std::optionalの値の有無のチェック

次に、std::optionalが値を持っているかどうかをチェックする方法を見てみましょう。

下記のコードは、std::optionalが値を持っているかどうかをチェックし、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。

#include <iostream>
#include <optional>

int main() {
    std::optional<int> value1 = 42;
    std::optional<int> value2;

    if (value1.has_value()) {
        std::cout << "value1 has a value: " << value1.value() << std::endl;
    } else {
        std::cout << "value1 has no value" << std::endl;
    }

    if (value2.has_value()) {
        std::cout << "value2 has a value: " << value2.value() << std::endl;
    } else {
        std::cout << "value2 has no value" << std::endl;
    }
}

実行結果

value1 has a value: 42
value2 has no value

has_value()メンバ関数を使えば、std::optionalが値を持っているかどうかをチェックできます。

値を持っている場合はtrue、持っていない場合はfalseを返します。

○サンプルコード3:std::optionalの値の取得

最後に、std::optionalから値を取得する方法を見てみましょう。

下記のコードは、std::optionalから値を取得し、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。

#include <iostream>
#include <optional>

int main() {
    std::optional<int> value1 = 42;
    std::optional<int> value2;

    std::cout << "value1: " << value1.value_or(0) << std::endl;
    std::cout << "value2: " << value2.value_or(0) << std::endl;
}

実行結果

value1: 42
value2: 0

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の基本的な使い方は、次のようになります。

#include <memory>

std::unique_ptr<int> ptr1 = std::make_unique<int>(42);  // int型のスマートポインターを作成し、42で初期化
std::unique_ptr<int> ptr2;                              // int型のスマートポインターを作成するが、初期値は与えない

std::unique_ptrは、テンプレート引数で指定した型のスマートポインターを作成できます。

上記の例では、int型のスマートポインターを作成しています。

ptr1は、std::make_unique()関数を使って42で初期化されているので、値を持っている状態になります。

一方、ptr2は初期値を与えていないので、値を持っていない状態になります。

○サンプルコード4:std::unique_ptrを使ったNullableなポインターの宣言

それでは実際に、std::unique_ptrを使ってNullableなポインターを宣言するサンプルコードを見てみましょう。

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::unique_ptr<int> ptr2;

    if (ptr1) {
        std::cout << "ptr1 has a value: " << *ptr1 << std::endl;
    } else {
        std::cout << "ptr1 has no value" << std::endl;
    }

    if (ptr2) {
        std::cout << "ptr2 has a value: " << *ptr2 << std::endl;
    } else {
        std::cout << "ptr2 has no value" << std::endl;
    }
}

実行結果

ptr1 has a value: 42
ptr2 has no value

ptr1は、std::make_unique()関数で初期化されているので、値を持っています。

そのため、if文の条件式でtrueとなり、ptr1の値が出力されます。

一方、ptr2は初期値を与えていないので、値を持っていません。

そのため、if文の条件式でfalseとなり、”ptr2 has no value”というメッセージが出力されます。

○サンプルコード5:std::unique_ptrの値の有無のチェック

次に、std::unique_ptrが値を持っているかどうかをチェックする方法を見てみましょう。

下記のコードは、std::unique_ptrが値を持っているかどうかをチェックし、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::unique_ptr<int> ptr2;

    if (ptr1) {
        std::cout << "ptr1 has a value: " << *ptr1 << std::endl;
    } else {
        std::cout << "ptr1 has no value, default value: 0" << std::endl;
    }

    if (ptr2) {
        std::cout << "ptr2 has a value: " << *ptr2 << std::endl;
    } else {
        std::cout << "ptr2 has no value, default value: 0" << std::endl;
    }
}

実行結果

ptr1 has a value: 42
ptr2 has no value, default value: 0

std::unique_ptrが値を持っているかどうかは、if文の条件式で直接チェックできます。

値を持っている場合はtrue、持っていない場合はfalseを返します。

○サンプルコード6:std::unique_ptrの値の取得

最後に、std::unique_ptrから値を取得する方法を見てみましょう。

下記のコードは、std::unique_ptrから値を取得し、値を持っている場合はその値を、持っていない場合はデフォルト値を出力する例です。

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::unique_ptr<int> ptr2;

    int value1 = ptr1 ? *ptr1 : 0;
    int value2 = ptr2 ? *ptr2 : 0;

    std::cout << "value1: " << value1 << std::endl;
    std::cout << "value2: " << value2 << std::endl;
}

実行結果は以下のようになります。

value1: 42
value2: 0

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クラスの例です。

#include <iostream>
#include <optional>
#include <string>

class Person {
public:
    Person(const std::string& name, std::optional<int> age)
        : m_name(name), m_age(age) {}

    std::string getName() const { return m_name; }
    std::optional<int> getAge() const { return m_age; }

private:
    std::string m_name;
    std::optional<int> m_age;
};

int main() {
    Person person1("Alice", 20);
    Person person2("Bob", std::nullopt);

    std::cout << "Name: " << person1.getName() << ", Age: ";
    if (person1.getAge()) {
        std::cout << *person1.getAge() << std::endl;
    } else {
        std::cout << "unknown" << std::endl;
    }

    std::cout << "Name: " << person2.getName() << ", Age: ";
    if (person2.getAge()) {
        std::cout << *person2.getAge() << std::endl;
    } else {
        std::cout << "unknown" << std::endl;
    }
}

実行結果

Name: Alice, Age: 20
Name: Bob, Age: unknown

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を返り値に持つ、リストから要素を検索するメソッドの例です。

#include <iostream>
#include <optional>
#include <vector>

std::optional<int> findElement(const std::vector<int>& list, int value) {
    for (int element : list) {
        if (element == value) {
            return element;
        }
    }
    return std::nullopt;
}

int main() {
    std::vector<int> list = {1, 2, 3, 4, 5};

    std::optional<int> result1 = findElement(list, 3);
    if (result1) {
        std::cout << "Found element: " << *result1 << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }

    std::optional<int> result2 = findElement(list, 6);
    if (result2) {
        std::cout << "Found element: " << *result2 << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }
}

実行結果

Found element: 3
Element not found

findElement()関数は、std::vector型のリストと、検索する値を受け取り、見つかった要素をstd::optional型で返します。

要素が見つからない場合は、std::nulloptを返します。

main()関数では、3と6を検索しています。3は見つかるので、”Found element: 3″と出力されます。

一方、6は見つからないので、”Element not found”と出力されます。

●Nullable型を使う際の注意点

これまで、Nullable型の実装方法やクラス設計における活用方法について解説してきましたが、実際にNullable型を使う際には、いくつか注意点があります。

ここでは、そんなNullable型を使う際の注意点について、具体的なサンプルコードを交えながら解説していきますので、ぜひ参考にしてみてくださいね。

○Nullable型の変数の初期化

Nullable型の変数を宣言する際には、初期値を明示的に指定することが重要です。

初期値を指定しないと、変数の状態が不定になってしまい、予期せぬバグの原因になることがあります。

例えば、下記のようなコードは、コンパイルは通りますが、望ましくありません。

std::optional<int> value;

この場合、valueがどのような状態にあるのかがわかりません。

値を持っているのか、それともstd::nulloptなのかが不明確です。

そのため、Nullable型の変数を宣言する際には、次のように初期値を明示的に指定するようにしましょう。

std::optional<int> value = 42;  // 値を持っている状態で初期化
std::optional<int> value = std::nullopt;  // 値を持っていない状態で初期化

このように初期値を明示的に指定することで、変数の状態が明確になり、バグを防ぐことができます。

○Nullable型の変数のデフォルト値

Nullable型の変数は、値を持っていない状態を表現できるため、デフォルト値を指定する必要がない場合があります。

例えば、次のような関数を考えてみましょう。

std::optional<int> getAge(const Person& person) {
    if (person.hasAge()) {
        return person.getAge();
    } else {
        return std::nullopt;
    }
}

この関数は、Personオブジェクトが年齢を持っている場合はその値を、持っていない場合はstd::nulloptを返します。

○Nullable型の変数の比較方法

Nullable型の変数を比較する際には、少し注意が必要です。

コードを見てみましょう。

○サンプルコード9:Nullable型の変数の比較方法

#include <iostream>
#include <optional>

int main() {
    std::optional<int> value1 = 42;
    std::optional<int> value2 = 42;
    std::optional<int> value3 = std::nullopt;

    if (value1 == value2) {
        std::cout << "value1 and value2 are equal" << std::endl;
    } else {
        std::cout << "value1 and value2 are not equal" << std::endl;
    }

    if (value1 == value3) {
        std::cout << "value1 and value3 are equal" << std::endl;
    } else {
        std::cout << "value1 and value3 are not equal" << std::endl;
    }
}

実行結果

value1 and value2 are equal
value1 and value3 are not equal

value1とvalue2は、どちらも42を持っているので、等しいと判定されます。

一方、value1は42を持っているのに対し、value3はstd::nulloptなので、等しくないと判定されます。

●Nullable型の応用例

これまでは、Nullable型の基本的な使い方や注意点について解説してきましたが、ここでは、Nullable型のより実践的な応用例を紹介していきます。

Nullable型は、様々な場面で活用できます。

例えば、キャッシュの実装、遅延初期化、オプション引数の処理などに使えます。

ここでは、そんなNullable型の応用例を、具体的なサンプルコードを交えて解説していきますので、ぜひ参考にしてみてくださいね。

○サンプルコード10:Nullable型を使ったキャッシュの実装

まずは、Nullable型を使ったキャッシュの実装例を見てみましょう。

キャッシュは、計算コストの高い処理の結果を保存しておくことで、同じ計算を繰り返すことを避けるためのテクニックです。

下記のコードは、フィボナッチ数列の計算結果をキャッシュするためにNullable型を使った例です。

#include <iostream>
#include <vector>
#include <optional>

std::vector<std::optional<int>> cache(100);

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }

    if (cache[n]) {
        return *cache[n];
    }

    int result = fibonacci(n - 1) + fibonacci(n - 2);
    cache[n] = result;
    return result;
}

int main() {
    std::cout << "fibonacci(10) = " << fibonacci(10) << std::endl;
    std::cout << "fibonacci(20) = " << fibonacci(20) << std::endl;
    std::cout << "fibonacci(30) = " << fibonacci(30) << std::endl;
}

実行結果

fibonacci(10) = 55
fibonacci(20) = 6765
fibonacci(30) = 832040

このコードでは、std::vectorを使ってキャッシュ用の配列cacheを定義しています。

cacheの要素はstd::optional型なので、計算結果が保存されていない場合はstd::nulloptになります。

fibonacci()関数では、まずcache[n]に計算結果が保存されているかをチェックします。

保存されていれば、その値を返します。保存されていない場合は、再帰的にフィボナッチ数列を計算し、その結果をcache[n]に保存してから返します。

このように、Nullable型を使えば、キャッシュの実装がシンプルになります。

キャッシュに値が保存されているかどうかを、Nullable型の値の有無で判定できるからです。

○サンプルコード11:Nullable型を使った遅延初期化の実装

次に、Nullable型を使った遅延初期化の実装例を見てみましょう。

遅延初期化とは、変数の初期化を、実際に変数が使用されるまで遅らせるテクニックです。

これで、不要な初期化処理を避けることができます。

下記のコードは、Nullable型を使って遅延初期化を実装した例です。

#include <iostream>
#include <optional>

class Widget {
public:
    Widget(int id) : m_id(id) {
        std::cout << "Constructor called for Widget " << m_id << std::endl;
    }

    ~Widget() {
        std::cout << "Destructor called for Widget " << m_id << std::endl;
    }

    void doSomething() {
        std::cout << "Widget " << m_id << " is doing something" << std::endl;
    }

private:
    int m_id;
};

class WidgetManager {
public:
    void doSomethingWithWidget(int id) {
        if (!m_widget) {
            m_widget = Widget(id);
        }
        m_widget->doSomething();
    }

private:
    std::optional<Widget> m_widget;
};

int main() {
    WidgetManager manager;
    manager.doSomethingWithWidget(1);
    manager.doSomethingWithWidget(2);
}

実行結果

Constructor called for Widget 1
Widget 1 is doing something
Destructor called for Widget 1
Constructor called for Widget 2
Widget 2 is doing something
Destructor called for Widget 2

このコードでは、WidgetManagerクラスのm_widget変数をstd::optional型にすることで、Widgetオブジェクトの初期化を遅延させています。

doSomethingWithWidget()メソッドが呼ばれると、まずm_widgetが値を持っているかをチェックします。

値を持っていない場合は、新しいWidgetオブジェクトを作成して、m_widgetに保存します。

そして、m_widget->doSomething()を呼び出します。

このように、Nullable型を使えば、遅延初期化を簡単に実装できます。

必要になるまで初期化を遅らせることで、無駄な初期化処理を避けられます。

○サンプルコード12:Nullable型を使ったオプション引数の実装

最後に、Nullable型を使ったオプション引数の実装例を見てみましょう。

オプション引数とは、省略可能な引数のことです。

Nullable型を使えば、オプション引数を簡単に実装できます。

下記のコードは、Nullable型を使ってオプション引数を実装した例です。

#include <iostream>
#include <optional>
#include <string>

void greet(const std::string& name, std::optional<std::string> message = std::nullopt) {
    std::cout << "Hello, " << name << "!" << std::endl;
    if (message) {
        std::cout << *message << std::endl;
    }
}

int main() {
    greet("Alice");
    greet("Bob", "How are you?");
}

実行結果

Hello, Alice!
Hello, Bob!
How are you?

このコードでは、greet()関数の第2引数messageをstd::optional型にすることで、オプション引数にしています。messageに値が渡された場合は、その値を出力します。

渡されなかった場合は、何も出力しません。

このように、Nullable型を使えば、オプション引数を簡単に実装できます。

省略可能な引数をNullable型にすることで、引数の有無を簡単に判定できるようになります。

まとめ

皆さん、ここまでお読みいただき、ありがとうございました

本記事では、C++でのNullable型の概要と利用方法について、詳しく解説してきました。

Nullable型を使う際には、変数の初期化やデフォルト値、比較方法などに注意が必要ですが、適切に使いこなすことで、C++プログラミングの幅が広がります。

皆さんのC++プログラミングの参考になれば幸いです。