C ++の不変データ

こんにちは、Habr! 不変データについては多くの話がありますが、C ++実装について何かを見つけるのは困難です。 そのため、デビュー記事のこのギャップを埋めることにしました。 さらに、Dにはありますが、C ++にはありません。 たくさんのコードとたくさんの手紙があるでしょう。







スタイルについて-ユーティリティクラスとメタ関数はSTLの名前を使用し、主にQtスタイルのカスタムクラスとブーストスタイルを使用します。







はじめに



不変データとは何ですか? 不変データは旧友のconstであり 、より厳密です。 理想的には、不変性とは、状況に依存しないコンテキストに依存しない不変性を意味します。







本質的に不変のデータは次のようにする必要があります。









不変データは関数型プログラミングから来ており、副作用がないことを保証するため、並列プログラミングの場所を見つけました。







C ++で不変データを実装するにはどうすればよいですか?

C ++では、次のことができます(大幅に簡略化されています)。









関数とvoidは不変ではありません。 リンクも不変にしません。これにはconst reference_wrapperがあります









上記の残りのタイプについては、ラッパー(または非標準の保護代替)を作成できます。 結果はどうなりますか? 目標は、このタイプのオブジェクトを操作するための自然なセマンティクスを維持しながら、タイプ修飾子を作成することです。







Immutable<int> a(1), b(2); qDebug() << (a + b).value() << (a + 1).value() << (1 + a).value(); int x[] = { 1, 2, 3, 4, 5 }; Immutable<decltype(x)> arr(x); qDebug() << arr[0]
      
      





インターフェース



一般的なインターフェースはシンプルです-すべての作業は、特性から派生した基本クラスによって行われます:







 template <typename Type> class Immutable : public immutable::immutable_impl<Type>::type { public: static_assert(!std::is_same<Type, std::nullptr_t>::value, "nullptr_t cannot used for immutable"); static_assert(!std::is_volatile<Type>::value, "volatile data cannot used for immutable"); using ImplType = typename immutable::immutable_impl<Type>; using BaseClass = typename ImplType::type; using BaseClass::BaseClass; using value_type = typename ImplType::value_type; constexpr Immutable& operator=(const Immutable &) = delete; };
      
      





代入演算子を無効にすることで、移動代入演算子を禁止しますが、移動コンストラクターを禁止しません。







immutable_implはスイッチのようなものですが、タイプによって異なります(私はこれをしませんでした-コードが複雑になりすぎ、単純な場合には実際には必要ありません-私見)。







 namespace immutable { template <typename SrcType> struct immutable_impl { using Type = std::remove_reference_t<SrcType>; using type = std::conditional_t< std::is_array<Type>::value, array<Type>, std::conditional_t < std::is_pointer<Type>::value, pointer<Type>, std::conditional_t < is_smart_pointer<Type>::value, smart_pointer<Type>, immutable_value<Type> > > >; using value_type = typename type::value_type; }; }
      
      





制限として、すべての割り当て操作を明示的に禁止します(マクロヘルプ):







 template <typename Type, typename RhsType> constexpr Immutable<Type>& operator Op=(Immutable<Type> &&, RhsType &&) = delete;
      
      





次に、個々のコンポーネントがどのように実装されるかを見てみましょう。







不変の値



値(以下、値)は、基本型のオブジェクト、クラスのインスタンス(構造、関連付け)、列挙として理解されます。 値の場合、型がクラス、構造体、または共用体であるかどうかを決定するクラスはありません。







 template <typename Type, bool = std::is_class<Type>::value || std::is_union<Type>::value> class immutable_value;
      
      





はいの場合、CRTPが実装に使用されます。







 template <typename Base> class immutable_value<Base, true> : private Base { public: using value_type = Base; constexpr explicit immutable_value(const Base &value) : Base(value) , m_value(value) { } constexpr explicit operator Base() const { return value(); } constexpr Base operator()() const { return value(); } constexpr Base value() const { return m_value; } private: const Base m_value; };
      
      





残念ながら、C ++には演算子のオーバーロードはまだありません 。 ただし、これはC ++ 17( http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0252r0.pdf、http://open-std.org/JTC1/SC22/ WG21 / docs / papers / 2016 / p0252r0.pdfhttp: //www.open-std.org/JTC1/SC22/wg21/docs/papers/2015/p0060r0.html ) 。

その後、あなたは書くことができます:







  constexpr Base operator.() const { return value(); }
      
      





しかし、この問題に関する決定は3月に予定されているため、この目的のために演算子()を使用します。







  constexpr Base operator()() const { return value(); }
      
      





コンストラクターに注意してください:~~







  constexpr explicit immutable_value(const Base &value) : Base(value) , m_value(value) { }
      
      





そこでは、immutable_valueと基本クラスの両方が初期化されます。 これにより、演算子()を使用してimmutable_valueを有意義に操作できます。 例:







 QPoint point(100, 500); Immutable<QPoint> test(point); test().setX(1000); //     qDebug() << test().isNull() << test().x() << test().y();
      
      





型がビルトインの場合、実装は基本クラスを除いて1対1になります(以下から可能になります) b DRYに一致するように戻りますが、特にimmutable_valueが残りの後に行われたため、何とか複雑にしたくありませんでした...):







 template <typename Type> class immutable_value<Type, false> { public: using value_type = Type; constexpr explicit immutable_value(const Type &value) : m_value(value) { } constexpr explicit operator Type() const { return value(); } constexpr Type operator()() const { return value(); } // Base operator . () const // { // return value(); // } constexpr Type value() const { return m_value; } private: const Type m_value; };
      
      





不変配列



これまでのところ、単純で面白くないように見えますが、今は配列を取り上げます。 STL(免疫力を弱める可能性がある)での作業を含む、配列を操作する自然なセマンティクスを保持しながら、std :: arrayのようなことを行う必要があります。







実装の特性は、インデックスごとに多次元インデックスにアクセスすると、低次元の配列が返されることであり、これも不変です。 配列型は再帰的にインスタンス化されます。演算子[]を参照してください。イテレータなどの特定の型は、array_traitsを使用して出力されます。







 namespace immutable { template <typename Tp> class array; template <typename ArrayType> struct array_traits; template <typename Tp, std::size_t Size> class array<Tp[Size]> { typedef Tp* pointer_type; typedef const Tp* const_pointer; public: using array_type = const Tp[Size]; using value_type = typename array_traits<array_type>::value_type; using size_type = typename array_traits<array_type>::size_type; using iterator = array_iterator<array_type>; using const_iterator = array_iterator<array_type>; using const_reverse_iterator = std::reverse_iterator<const_iterator>; constexpr explicit array(array_type &&array) : m_array(std::forward<array_type>(array)) { } constexpr explicit array(array_type &array) : m_array(array) { } ~array() = default; constexpr size_type size() const noexcept { return Size; } constexpr bool empty() const noexcept { return size() == 0; } constexpr const_pointer value() const noexcept { return data(); } constexpr value_type operator[](size_type n) const noexcept { return value_type(m_array[n]); } //       constexpr value_type at(size_type n) const { return n < Size ? operator [](n) : out_of_range(); } const_iterator begin() const noexcept { return const_iterator(m_array.get()); } const_iterator end() const noexcept { return const_iterator(m_array.get() + Size); } const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } const_iterator cbegin() const noexcept { return const_iterator(data()); } const_iterator cend() const noexcept { return const_iterator(data() + Size); } const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(end()); } const_reverse_iterator crend() const noexcept { return const_reverse_iterator(begin()); } constexpr value_type front() const noexcept { return *begin(); } constexpr value_type back() const noexcept { return *(end() - 1); } private: constexpr pointer_type data() const noexcept { return m_array.get(); } [[noreturn]] constexpr value_type out_of_range() const { throw std::out_of_range("array: out of range");} private: const std::reference_wrapper<array_type> m_array; }; }
      
      





特性のクラスを使用して、低次元のタイプを決定します。







 namespace immutable { template <typename ArrayType, std::size_t Size> struct array_traits<ArrayType[Size]> { using value_type = std::conditional_t<std::rank<ArrayType[Size]>::value == 1, ArrayType, array<ArrayType> // immutable::array >; using size_type = std::size_t; }; }
      
      





これは、インデックス付けがより小さい次元の不変配列を返すときの多次元配列の場合です。







比較演算子は非常に簡単です。







比較演算子
 template<typename Tp, std::size_t Size> inline bool operator==(const array<Tp[Size]>& one, const array<Tp[Size]>& two) { return std::equal(one.begin(), one.end(), two.begin()); } template<typename Tp, std::size_t Size> inline bool operator!=(const array<Tp[Size]>& one, const array<Tp[Size]>& two) { return !(one == two); } template<typename Tp, std::size_t Size> inline bool operator<(const array<Tp[Size]>& a, const array<Tp[Size]>& b) { return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); } template<typename Tp, std::size_t Size> inline bool operator>(const array<Tp[Size]>& one, const array<Tp[Size]>& two) { return two < one; } template<typename Tp, std::size_t Size> inline bool operator<=(const array<Tp[Size]>& one, const array<Tp[Size]>& two) { return !(one > two); } template<typename Tp, std::size_t Size> inline bool operator>=(const array<Tp[Size]>& one, const array<Tp[Size]>& two) { return !(one < two); }
      
      





不変のイテレーター



不変配列を操作するには、不変array_iteratorイテレーターが使用されます。







 namespace immutable { template <typename Tp> class array; template <typename Array> class array_iterator : public std::iterator<std::bidirectional_iterator_tag, Array> { public: using element_type = std::remove_extent_t<Array>; using value_type = std::conditional_t< std::rank<Array>::value == 1, element_type, array<element_type> >; using ptr_to_array_type = const element_type *; static_assert(std::is_array<Array>::value, "Substitution error: template argument must be array"); constexpr array_iterator(ptr_to_array_type ptr) : m_ptr(ptr) { } constexpr value_type operator *() const { return value_type(*m_ptr);} constexpr array_iterator operator++() { ++m_ptr; return *this; } constexpr array_iterator operator--() { --m_ptr; return *this; } constexpr bool operator == (const array_iterator &other) const { return m_ptr == other.m_ptr; } private: ptr_to_array_type m_ptr; }; template <typename Array> inline constexpr array_iterator<Array> operator++(array_iterator<Array> &it, int) { auto res = it; ++it; return res; } template <typename Array> inline constexpr array_iterator<Array> operator--(array_iterator<Array> &it, int) { auto res = it; --it; return res; } template <typename Array> inline constexpr bool operator != (const array_iterator<Array> &a, const array_iterator<Array> &b) { return !(a == b); } }      ,     .  ,  - :
      
      





不変配列のコード例
 int x[5] = { 1, 2, 3, 4, 5 }; int y[5] = { 1, 2, 3, 4, 5 }; immutable::array<decltype(x)> a(x); immutable::array<decltype(y)> b(y); qDebug() << (a == b); const char str[] = "abcdef"; immutable::array<decltype(str)> imstr(str); auto it = imstr.begin(); while(*it) qDebug() << *it++;
      
      





多次元配列の場合、すべてが同じです。







多次元不変配列の例
 int y[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } }; int z[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } }; immutable::array<decltype(y)> b(y); immutable::array<decltype(z)> c(z); for(auto row = b.begin(); row != b.end(); ++row) { qDebug() << "(*row)[0]" << (*row)[0]; } for(int i = 0; i < 2; ++i) for(int j = 0; j < 2; ++j) qDebug() << b[i][j]; qDebug() << (b == c); for(auto row = b.begin(); row != b.end(); ++row) { for(auto col = (*row).begin(); col != (*row).end(); ++col) qDebug() << *col; }
      
      





不変のポインター



ポインターをわずかに保護してみましょう。 このセクションでは、通常のポインター(生のポインター)を調べ、次に(さらに)スマートポインターを見ていきます。 スマートポインターには、SFINAEが使用されます。







不変::ポインターの実装に関して、ポインターはデータを削除せず、リンクを考慮せず、オブジェクトの不変性を保証するだけであるとすぐに言います。 (渡されたポインターが外部から変更または削除された場合、これは契約違反であり、言語を使用して(標準的な方法で)追跡することはできません)。 最終的には、意図的な妨害行為やアドレスでの遊びから身を守ることは不可能です。 ポインターを正しく初期化する必要があります。







不変::ポインターは、どの程度の参照性のポインターへのポインターでも機能します(そういうことです)。







例:







不変ポインターの例
 immutable::pointer<QApplication*> app(&a); app->quit(); char c = 'A'; char *pc = &c; char **ppc = &pc; char ***pppc = &ppc; immutable::pointer<char***> x(pppc); qDebug() << ***x;
      
      





上記に加えて、不変::ポインターはCスタイルの文字列の操作をサポートしていません。







 const char *cstr = "test"; immutable::pointer<decltype(str)> p(cstr); while(*p++) qDebug() << *p;
      
      





このコードは期待どおりに機能しません。 immutable ::ポインターは、インクリメントすると、異なるアドレスを持つ新しい不変::ポインターを返します。条件式では、インクリメントの結果がチェックされます。 文字列の2番目の文字の値。







実装に戻ります。 ポインタークラスは共通のインターフェイスを提供し、Tpの種類(ポインターへのポインターまたはプロトポインター)に応じて、pointer_implの特定の実装を使用します。







  template <typename Tp> class pointer { public: static_assert( std::is_pointer<Tp>::value, "Tp must be pointer"); static_assert(!std::is_volatile<Tp>::value, "Tp must be nonvolatile pointer"); static_assert(!std::is_void<std::remove_pointer_t<Tp>>::value, "Tp can't be void pointer"); typedef Tp source_type; typedef pointer_impl<Tp> pointer_type; typedef typename pointer_type::value_type value_type; constexpr explicit pointer(Tp ptr) : m_ptr(ptr) { } constexpr pointer(std::nullptr_t) = delete; //    0 ~pointer() = default; constexpr const pointer_type value() const { return m_ptr; } /** * @brief operator =  , . const *const  *  . *   ,     , *        , *    " = delete"   ,   *     */ pointer& operator=(const pointer&) = delete; constexpr /*immutable<value_type>*/ value_type operator*() const { return *value(); } constexpr const pointer_type operator->() const { return value(); } //   template <typename T> constexpr operator T() = delete; template <typename T> constexpr operator T() const = delete; /** * @brief operator []   ,     *  . * *  - -   *         * (  ) * @return */ template <typename Ret = std::remove_pointer_t<Tp>, typename IndexType = ssize_t> constexpr Ret operator[](IndexType) const = delete; constexpr bool operator == (const pointer &other) const { return value() == other.value(); } constexpr bool operator < (const pointer &other) const { return value() < other.value(); } private: const pointer_type m_ptr; };
      
      





本質は次のとおりです。型T があり、そのストレージ/表現には、pointer_impl <T 、true> の実装が使用されます(テンプレート再帰) 。これは次のように表すことができます。







 pointer_impl<T***, true>{ pointer_impl<T**, true> { pointer_impl<T*, false> { const T *const } } }
      
      





合計、結局のところ:const T const const * const。







単純なポインター(別のポインターを指さない)の場合、実装は次のとおりです。







  template <typename Type> class pointer_impl<Type, false> { public: typedef std::remove_pointer_t<Type> source_type; typedef source_type *const pointer_type; typedef source_type value_type; constexpr pointer_impl(Type value) : m_value(value) { } constexpr value_type operator*() const noexcept { return *m_value; // *    } constexpr bool operator == (const pointer_impl &other) const noexcept { return m_value == other; } constexpr bool operator < (const pointer_impl &other) const noexcept { return m_value < other; } constexpr const pointer_type operator->() const noexcept { using class_type = std::remove_pointer_t<pointer_type>; static_assert(std::is_class<class_type>::value || std::is_union<class_type>::value , "-> used only for class, union or struct"); return m_value; } private: const pointer_type m_value; };
      
      





ネストされたポインター(ポインターへのポインター)の場合:







  template <typename Type> class pointer_impl<Type, true> { public: typedef std::remove_pointer_t<Type> source_type; typedef pointer_impl<source_type> pointer_type; typedef pointer_impl<source_type> value_type; constexpr /* implicit */ pointer_impl(Type value) : m_value(*value) { // /\ remove pointer } constexpr bool operator == (const pointer_impl &other) const { return m_value == other; //   } constexpr bool operator < (const pointer_impl &other) const { return m_value < other; //   } constexpr value_type operator*() const { return value_type(m_value); //   } constexpr const pointer_type operator->() const { return m_value; } private: const pointer_type m_value; };
      
      





してはいけないこと!

次のタイプのポインターの場合、特殊化する価値はありません。







  • 配列へのポインタ(*)[];
  • 関数ポインター(*)(引数... [...]);
  • クラス変数へのポインタ、Class ::は非常に特殊なもので、クラスとの「魔術」に必要であり、オブジェクトに関連付けられている必要があります。

    -クラスメソッドへのポインタ(Class::) (Args ... [...])[const] [volatile]。


不変のスマートポインター



スマートポインターがあることを確認する方法 スマートポインターは、*および->演算子を実装します。 それらの可用性を判断するには、SFINAEを使用します(SFINAEの実装については後で検討します)。







 namespace immutable { // is_base_of<_Class, _Tp> template <typename Tp> class is_smart_pointer { DECLARE_SFINAE_TESTER(unref, T, t, t.operator*()); DECLARE_SFINAE_TESTER(raw, T, t, t.operator->()); public: static const bool value = std::is_class<Tp>::value && GET_SFINAE_RESULT(unref, Tp) && GET_SFINAE_RESULT(raw, Tp); }; }
      
      





演算子->を介して、間接的なアクセスを使用すると、特にクラスに変更可能なデータがある場合は、不変性に違反する可能性があることをすぐに言います。 さらに、戻り値の不変性は、コンパイラ(型が出力される場合)とユーザーの両方によって削除できます。







実装-ここではすべてが簡単です:







 namespace immutable { template <typename Type> class smart_pointer { public: constexpr explicit smart_pointer(Type &&ptr) noexcept : m_value(std::forward<Type>(ptr)) { } constexpr explicit smart_pointer(const Type &ptr) : m_value(ptr) { } constexpr const auto operator->() const { const auto res = value().operator->(); return immutable::pointer<decltype(res)>(res);// in C++17 immutable::pointer(res); } constexpr const auto operator*() const { return value().operator*(); } constexpr const Type value() const { return m_value; } private: const Type m_value; }; }
      
      





SFINAE



それは何であり、何を食べているのか、もう一度説明する必要はありません。 SFINAEを使用すると、クラスにメソッド、メンバー型などが含まれているかどうか、さらにオーバーロードされた関数が存在するかどうかを判断できます(testexpr式で、必要なパラメーターを使用して目的の関数の呼び出しを指定する場合)。 argは空で、testexprに参加しない場合があります。 型でSFINAEを使用し、式でSFINAEを使用します。







 #define DECLARE_SFINAE_BASE(Name, ArgType, arg, testexpr) \ typedef char SuccessType; \ typedef struct { SuccessType a[2]; } FailureType; \ template <typename ArgType> \ static decltype(auto) test(ArgType &&arg) \ -> decltype(testexpr, SuccessType()); \ static FailureType test(...); #define DECLARE_SFINAE_TESTER(Name, ArgType, arg, testexpr) \ struct Name { \ DECLARE_SFINAE_BASE(Name, ArgType, arg, testexpr) \ }; #define GET_SFINAE_RESULT(Name, Type) (sizeof(Name::test(std::declval<Type>())) == \ sizeof(typename Name::SuccessType))
      
      





そしてもう1つ:署名が一致するが、3つのフェーズでSFINAEと組み合わせて修飾子const [volatile]またはvolatileが異なる場合、オーバーロードを有効にできます(必要なオーバーロード関数を見つけます)。







1)SFINAE-あれば、OK

2)SFINAE + QNonConstOverload、動作しなかった場合、

3)SFINAE + QConstOverload







Qtソースには、興味深く有用なものがあります。







constによるオーバーロード解決
  template <typename... Args> struct QNonConstOverload { template <typename R, typename T> Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } template <typename R, typename T> static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } }; template <typename... Args> struct QConstOverload { template <typename R, typename T> Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...) const) const Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } template <typename R, typename T> static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...) const) Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } }; template <typename... Args> struct QOverload : QConstOverload<Args...>, QNonConstOverload<Args...> { using QConstOverload<Args...>::of; using QConstOverload<Args...>::operator(); using QNonConstOverload<Args...>::of; using QNonConstOverload<Args...>::operator(); template <typename R> Q_DECL_CONSTEXPR auto operator()(R (*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } template <typename R> static Q_DECL_CONSTEXPR auto of(R (*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr) { return ptr; } };
      
      





まとめ



何が起こったのか試してみましょう:







 QPoint point(100, 500); Immutable<QPoint> test(point); test().setX(1000); //     qDebug() << test().isNull() << test().x() << test().y(); int x[] = { 1, 2, 3, 4, 5 }; Immutable<decltype(x)> arr(x); qDebug() << arr[0];
      
      





オペレーター



演算子について思い出しましょう! たとえば、加算演算子のサポートを追加します。

最初に、 Immutable<Type>



+ Typeという形式の加算演算子を実装します。







 template <typename Type> inline constexpr Immutable<Type> operator+(const Immutable<Type> &a, Type &&b) { return Immutable<Type>(a.value() + b); }
      
      





代わりにC ++ 17で







  return Immutable<Type>(a.value() + b);
      
      





書ける







 return Immutable(a.value() + b);
      
      





なぜなら +演算子は可換であるため、Type + Immutable<Type>



は次のように実装できます。







 template <typename Type> inline constexpr Immutable<Type> operator+(Type &&a, const Immutable<Type> &b) { return b + std::forward<Type>(a); }
      
      





そして再び、最初のフォームを通してImmutable<Type>



+ Immutable<Type>



を実装しImmutable<Type>









 template <typename Type> inline constexpr Immutable<Type> operator+(const Immutable<Type> &a, const Immutable<Type> &b) { return a + b.value(); }
      
      





これで作業できます:







 Immutable<int> a(1), b(2); qDebug() << (a + b).value() << (a + 1).value() << (1 + a).value();
      
      





同様に、残りの操作を定義できます。 アドレスを取得する演算子&&、||!をオーバーロードしないでください。 単項+、-、!、〜便利になるかもしれません...これらの操作は継承されます:()、[]、->、-> (単項)。







比較演算子はブール型の値を返す必要があります。







比較演算子
 template <typename Type> inline constexpr bool operator==(const Immutable<Type> &a, const Immutable<Type> &b) { return a.value() == b.value(); } template <typename Type> inline constexpr bool operator!=(const Immutable<Type> &a, const Immutable<Type> &b) { return !(a == b); } template <typename Type> inline constexpr bool operator>(const Immutable<Type> &a, const Immutable<Type> &b) { return a.value() > b.value(); } template <typename Type> inline constexpr bool operator<(const Immutable<Type> &a, const Immutable<Type> &b) { return b < a; } template <typename Type> inline constexpr bool operator>=(const Immutable<Type> &a, const Immutable<Type> &b) { return !(a < b); } template <typename Type> inline constexpr bool operator<=(const Immutable<Type> &a, const Immutable<Type> &b) { return !(b < a); }
      
      






All Articles