C++/標準ライブラリ/any
はじめに
[編集]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 }
型消去とは、ある型のオブジェクトをラップし、その実際の型情報を隠蔽する操作のことです。型消去の結果得られるオブジェクトは、元の型の持つインターフェースからのみアクセス可能になります。このようにして、実際の型には依存しない抽象化されたインターフェースを実現できます。
型消去の典型的な例がstd::any
とstd::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&&...);
any
にT
型のオブジェクトをインプレース構築し、参照を返します。コンストラクタ引数を渡せます。- 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で導入された、任意の型のオブジェクトを格納できる汎用的なコンテナクラスです。型消去の概念を使い、実行時に格納する型を決定できるため、コンテナや関数の引数に様々な型の値を渡せます。一方で、コピー可能な型に限定されたり、実行時オーバーヘッドが発生したりと制約もあります。用途に合わせて、適切に使い分ける必要があります。