コンテンツにスキップ

C++/標準ライブラリ/any

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

C++17から導入されたstd::anyクラスは、任意の型のオブジェクトを格納できる汎用的なコンテナクラスです。型消去(type erasure)の概念を使い、実行時に格納する型を決定できます。これにより、コンテナや関数の引数にさまざまな型の値を渡すことができます。

#include <any>
#include <iostream>

auto main() -> int {
    std::any a = 1;     // 整数
    std::any b = 3.14;  // 浮動小数点数
    std::any c = true;  // 真理値

    std::cout << std::any_cast<int>(a) << std::endl;                     // 1
    std::cout << std::any_cast<double>(b) << std::endl;                  // 3.14
    std::cout << std::boolalpha << std::any_cast<bool>(c) << std::endl;  // true
}
型消去 (Type Erasure)
オブジェクト指向プログラミングにおける多態性は、仮想関数を使うことで実現されます。しかし、テンプレートクラスのように対象のクラスが事前に確定していない状況では、仮想関数を使った多態性は適用できません。こういった場合に型消去(Type Erasure)の手法が役立ちます。

型消去とは、ある型のオブジェクトをラップし、その実際の型情報を隠蔽する操作のことです。型消去の結果得られるオブジェクトは、元の型の持つインターフェースからのみアクセス可能になります。このようにして、実際の型には依存しない抽象化されたインターフェースを実現できます。

型消去の典型的な例がstd::anystd::functionです。

std::anyは、任意の型のオブジェクトを格納できるクラスで、C++17で導入されました。格納されたオブジェクトの実際の型は隠蔽され、any_castなどの特別な関数を使うことでのみアクセスできます。これにより、コンテナや関数の引数に異なる型の値を渡せるようになりました。

また、std::functionは関数ポインタのラッパークラスです。関数ポインタの型を隠蔽することで、関数の実体を知らずに呼び出せるようになります。これは、ジェネリックなコールバック関数の実装などに役立ちます。

型消去は多態性の代替手段としても使えますが、実行時にオーバーヘッドが発生するというデメリットもあります。std::anyはオブジェクトのコピーが必要になるので、小さな値型オブジェクトを格納する場合は特にオーバーヘッドが大きくなります。また、型情報が完全に失われるため、元の型のメンバ関数にアクセスしたりポインタを経由した操作はできません。

一方で、従来の仮想関数による多態性では、全ての関数が仮想関数でなければならず、オーバーヘッドが避けられないという制約があります。型消去はこの制約を払拭し、必要な時にのみオーバーヘッドを払うことができます。

このように、型消去は一般化されたインターフェースを提供する強力な手段ですが、メリット・デメリットを理解した上で使い分ける必要があります。

anyクラス

[編集]

std::anyクラスは、任意の型のオブジェクトを格納できるコンテナクラスです。主な機能は以下のとおりです。

コンストラクタ

[編集]
デフォルトコンストラクタ
constexpr any() noexcept;
空のanyオブジェクトを構築します。
コピーコンストラクタ
any(const any& other);
他のanyオブジェクトからコピー構築します。格納されているオブジェクトのコピーコンストラクタが呼び出されます。
ムーブコンストラクタ
any(any&& other) noexcept;
他のanyオブジェクトからムーブ構築します。格納されているオブジェクトのムーブコンストラクタが呼び出されます。
値からのコンストラクタ
template<class T>
any(T&& value);
値からの直接構築です。T型の一時オブジェクトが構築され、anyに格納されます。
型からのコンストラクタ
template<class T, class... Args>
explicit any(in_place_type_t<T>, Args&&...);

template<class T, class U, class... Args>
explicit any(in_place_type_t<T>, initializer_list<U>, Args&&...);
T型のオブジェクトをインプレース構築して、anyに格納します。コンストラクタ引数を渡せます。

代入演算子

[編集]
コピー代入演算子
any& operator=(const any& rhs);
他のanyオブジェクトからコピー代入します。事前に格納されていたオブジェクトの破棄、新しい型のオブジェクトの構築が行われます。
ムーブ代入演算子
any& operator=(any&& rhs) noexcept;
他のanyオブジェクトからムーブ代入します。事前に格納されていたオブジェクトの破棄、新しい型のオブジェクトのムーブ構築が行われます。
値からの代入
template<class T>
any& operator=(T&& rhs);
T型の値から直接代入します。事前に格納されていたオブジェクトの破棄、T型のオブジェクトの一時構築後、ムーブ代入されます。

メンバー関数

[編集]
emplace() - インプレース構築
template<class T, class... Args>
decay_t<T>& emplace(Args&&...);

template<class T, class U, class... Args>
decay_t<T>& emplace(initializer_list<U>, Args&&...);
anyT型のオブジェクトをインプレース構築し、参照を返します。コンストラクタ引数を渡せます。
reset() - リセット
void reset() noexcept;
格納されているオブジェクトを破棄し、空の状態にリセットします。
swap() - スワップ
void swap(any& rhs) noexcept;
他のanyオブジェクトと格納しているオブジェクトを交換します。
has_value() - 値が設定されているかの確認
bool has_value() const noexcept;
anyが値を格納している場合はtrueを返します。
type() - 格納された型の取得
const type_info& type() const noexcept;
格納されているオブジェクトの型情報を返します。typeidを使った型の比較に使えます。

メンバー型エイリアス

[編集]

anyクラスには、メンバー型エイリアスは定義されていません。

bad_any_cast例外クラス

[編集]

std::any_cast関数で型が一致しない場合に投げられる例外クラスです。std::bad_castから派生しています。

namespace std {
class bad_any_cast : public bad_cast {
public:
    const char* what() const noexcept override;
};
}

非メンバー関数

[編集]

std::anyクラスに関連する主な非メンバー関数は以下のとおりです。

swap()
void swap(any& x, any& y) noexcept;
2つのanyオブジェクトを入れ替えます。x.swap(y)と同等の操作です。
make_any()
template<class T, class... Args>
any make_any(Args&&... args);

template<class T, class U, class... Args>
any make_any(initializer_list<U> il, Args&&... args);
T型のオブジェクトを構築し、anyに格納したオブジェクトを返します。コンストラクタ引数を渡せます。
any_cast()
template<class T>
T any_cast(const any& operand);

template<class T>  
T any_cast(any& operand); 

template<class T>
T any_cast(any&& operand);

template<class T>
const T* any_cast(const any* operand) noexcept;

template<class T>  
T* any_cast(any* operand) noexcept;
anyに格納されているオブジェクトをT型に参照または値で取り出します。型が一致しない場合、ポインタ版ならnullptrを返し、値版ならbad_any_cast例外を送出します。

anyクラスの利用例

[編集]

単純な値の格納・参照

[編集]

最も基本的な使い方として、異なる型の値をanyに格納し、後から適切な型に変換して参照することができます。

#include <any>
#include <any>
#include <iostream>

auto main() -> int {
    std::any a = 1;     // 整数
    std::any b = 3.14;  // 浮動小数点数
    std::any c = true;  // 真理値

    std::cout << std::any_cast<int>(a) << std::endl;                     // 1
    std::cout << std::any_cast<double>(b) << std::endl;                  // 3.14
    std::cout << std::boolalpha << std::any_cast<bool>(c) << std::endl;  // true
}

コンテナへの格納

[編集]

std::vectorなどのコンテナに、異なる型の値をanyとして格納できます。これにより、単一のコンテナに様々な型の値を保持できます。

#include <any>
#include <iostream>
#include <string>
#include <vector>

auto main() -> int {
    std::vector<std::any> values;
    values.emplace_back(1);        // 整数
    values.emplace_back(3.14);     // 浮動小数点数
    values.emplace_back(true);     // 真理値
    values.emplace_back("hello");  // 文字列

    for (const auto& v : values) {
        if (v.type() == typeid(int)) {
            std::cout << std::any_cast<int>(v) << std::endl;
        } else if (v.type() == typeid(double)) {
            std::cout << std::any_cast<double>(v) << std::endl;
        } else if (v.type() == typeid(bool)) {
            std::cout << std::any_cast<bool>(v) << std::endl;
        } else if (v.type() == typeid(std::string)) {
            std::cout << std::any_cast<std::string>(v) << std::endl;
        }
    }
}

多態性の実現

[編集]

std::anyを使うと、実行時に任意の型のオブジェクトを渡せるため、多態性を実現できます。

#include <any>
#include <iostream>
#include <string>

void print(const std::any& value) {
    if (value.type() == typeid(int)) {
        std::cout << std::any_cast<int>(value) << std::endl;
    } else if (value.type() == typeid(double)) {
        std::cout << std::any_cast<double>(value) << std::endl;
    } else if (value.type() == typeid(bool)) {
        std::cout << std::any_cast<bool>(value) << std::endl;
    } else if (value.type() == typeid(std::string)) {
        std::cout << std::any_cast<std::string>(value) << std::endl;
    }
}

auto main() -> int {
    print(1);                     // 整数
    print(3.14);                  // 浮動小数点数
    print(true);                  // 真理値
    print(std::string("hello"));  // 文字列
}

タグ付きUnionの代替

[編集]

従来、タグ付きUnionを使って値の型を区別していましたが、std::anyを使えばより安全で扱いやすくなります。

#include <any>
#include <iostream>
#include <string>

auto main() -> int {
    std::any value = 1;            // 整数
    value = 3.14;                  // 浮動小数点数に変更
    value = true;                  // 真理値に変更
    value = std::string("hello");  // 文字列に変更

    // 型を比較して適切にキャスト
    if (value.type() == typeid(int)) {
        std::cout << std::any_cast<int>(value) << std::endl;
    } else if (value.type() == typeid(double)) {
        std::cout << std::any_cast<double>(value) << std::endl;
    } else if (value.type() == typeid(bool)) {
        std::cout << std::any_cast<bool>(value) << std::endl;
    } else if (value.type() == typeid(std::string)) {
        std::cout << std::any_cast<std::string>(value) << std::endl;  // hello
    }
}

注意点と制約事項

[編集]

std::anyクラスを使う上での注意点と制約事項は以下のとおりです。

コピー可能な型に限定

[編集]

std::anyに格納できるのは、コピー構築可能な型に限られます。コピーコンストラクタを持たない型は格納できません。ムーブ専用の型は格納できますが、コピー操作はできなくなります。

スロー保証

[編集]

std::anyのコンストラクタとデストラクタはノウスロー (noexcept)ですが、代入演算子やスワップ、その他の操作は例外を送出する可能性があります。格納されているオブジェクトのコピー/ムーブ操作中に例外が発生した場合に、例外がそのまま伝播します。

空の状態

[編集]

std::anyは空の状態をもてます。デフォルト構築されたanyオブジェクトは空の状態になります。reset()を呼ぶと空の状態に戻ります。空の状態のanyに対してany_castを呼ぶとbad_any_cast例外が送出されます。

型消去

[編集]

std::anyは型消去により実装されています。つまり、格納されている実際の型情報は失われ、ポインタも保持されません。そのため、格納されている値に対して直接メンバー関数やポインタを経由した操作はできません。

実行時パフォーマンス

[編集]

std::anyは型消去のために実行時のオーバーヘッドが発生します。小さな値型オブジェクトを格納する場合、そのオーバーヘッドが相対的に大きくなります。大きな値型オブジェクトや動的メモリ確保を伴うオブジェクトを格納する場合は、そのオーバーヘッドは無視できるレベルになります。

その他の代替パターン

[編集]

std::anyに代わる手段として、以下のようなパターンが従来から使われてきました。

  • タグ付きUnion
  • void*ポインタと関数ポインタテーブル
  • ビジター(Visitor)パターン
  • オブジェクトのラッパークラス

これらの手法はstd::anyに比べてコーディングが面倒でエラーへの耐性が低いというデメリットがありますが、std::anyに比べて実行時オーバーヘッドが小さいというメリットがあります。用途に応じて適切な手法を選ぶ必要があります。

まとめ

[編集]

std::anyクラスはC++17で導入された、任意の型のオブジェクトを格納できる汎用的なコンテナクラスです。型消去の概念を使い、実行時に格納する型を決定できるため、コンテナや関数の引数に様々な型の値を渡せます。一方で、コピー可能な型に限定されたり、実行時オーバーヘッドが発生したりと制約もあります。用途に合わせて、適切に使い分ける必要があります。