Boost.Contract その2 - C++ Advent Calender 2012
その1の続きです。
宣言と実装の分離
基本構文はだいたい押さえましたね。しかし、実際に使うとなると大事なことを忘れています。
このまま行くと「あれ?そういえば宣言と実装分けられないのかな?」となるのは確実です。
Boost.Contractではそれが可能です。自分で書くと結構たいへんだったのでTutorialのを引用しちゃいました。
// body_natural.hpp // Copyright (C) 2008-2012 Lorenzo Caminiti // Distributed under the Boost Software License, Version 1.0 // (see accompanying file LICENSE_1_0.txt or a copy at // http://www.boost.org/LICENSE_1_0.txt) // Home at http://sourceforge.net/projects/contractpp #ifndef BODY_NATURAL_HPP_ #define BODY_NATURAL_HPP_ #include <contract.hpp> //[body_natural template< typename T, T Default = T() > // Class forward declaration. class natural ; template< typename T > // Function forward declaration. bool less ( natural<T> const& left, natural<T> const& right ) ; CONTRACT_FUNCTION( template( typename T ) bool (greater) ( (natural<T> const&) left, (natural<T> const&) right ) postcondition( auto result = return, result ? not less(left, right) : true ) ) ; // Deferred free function body definition, use `;`. CONTRACT_CLASS( // Class declaration (with contracts). template( typename T, (T) Default ) class (natural) ) { CONTRACT_CLASS_INVARIANT_TPL( get() >= 0 ) CONTRACT_CONSTRUCTOR_TPL( public explicit (natural) ( (T const&) value, default Default ) precondition( value >= 0 ) postcondition( get() == value ) // Unfortunately, no member initializers when body deferred. ) ; // Deferred constructor body definition, use `;`. CONTRACT_DESTRUCTOR_TPL( public (~natural) ( void ) ) ; // Deferred destructor body definition, use `;`. CONTRACT_FUNCTION_TPL( public bool (equal) ( (natural const&) right ) const postcondition( auto result = return, result == not less(*this, right) && not greater(*this, right) ) ) ; // Deferred member function body definition, use `;`. CONTRACT_FUNCTION_TPL( public (T) (get) ( void ) const ) { return value_; } private: T value_; }; CONTRACT_FUNCTION( // Function declaration (with contracts). template( typename T ) bool (less) ( (natural<T> const&) left, (natural<T> const&) right ) postcondition( auto result = return, result ? not greater(left, right) : true ) ) { return left.get() < right.get(); } //] #include "body_natural_impl.hpp" #endif // #include guard
// body_natural_impl.hpp // Copyright (C) 2008-2012 Lorenzo Caminiti // Distributed under the Boost Software License, Version 1.0 // (see accompanying file LICENSE_1_0.txt or a copy at // http://www.boost.org/LICENSE_1_0.txt) // Home at http://sourceforge.net/projects/contractpp #include "body_natural.hpp" //[body_natural_impl // Deferred body definitions, separated from their declarations and contracts. template< typename T > bool CONTRACT_FREE_BODY(greater) ( natural<T> const& left, natural<T> const& right ) { return left.get() > right.get(); } template< typename T, T Default > CONTRACT_CONSTRUCTOR_BODY((natural<T, Default>), natural) ( T const& value ) { value_ = value; } template< typename T, T Default > CONTRACT_DESTRUCTOR_BODY((natural<T, Default>), ~natural) ( void ) { // Do nothing. } template< typename T, T Default > bool natural<T, Default>::CONTRACT_MEMBER_BODY(equal) ( natural const& right ) const { return not less(*this, right) && not greater(*this, right); } //]
// main.cpp // Copyright (C) 2008-2012 Lorenzo Caminiti // Distributed under the Boost Software License, Version 1.0 // (see accompanying file LICENSE_1_0.txt or a copy at // http://www.boost.org/LICENSE_1_0.txt) // Home at http://sourceforge.net/projects/contractpp #include <boost/detail/lightweight_test.hpp> #include "body_natural.hpp" int main ( void ) { natural<int> n, m(123); BOOST_TEST(n.equal(natural<int>())); BOOST_TEST(less(n, m)); BOOST_TEST(greater(m, n)); return boost::report_errors(); }
あれ?結構難しい…?そう思ったら負けです。踏ん張りましょう。
templateの部分がちょっとややこしいかもしれませんが、普段のC++プログラミングと同じなので省略します。
順番に解説していきますね。
フリー関数の分離
CONTRACT_FUNCTION( template( typename T ) bool (greater) ( (natural<T> const&) left, (natural<T> const&) right ) postcondition( auto result = return, result ? not less(left, right) : true ) ) ; // Deferred free function body definition, use `;`.
template< typename T > bool CONTRACT_FREE_BODY(greater) ( natural<T> const& left, natural<T> const& right ) { return left.get() > right.get(); }
いつもなら宣言と同時に中身も書いていましたが、CONTRACT_FREE_BODYマクロを使うことで分離が可能になっています。
postconditionのところに not がありますが、これは「!」と同じだと考えれば分かると思います。!つかってもいいです。
実装側ではtemplateにマクロがかかってませんね。分離するときはマクロで包みません。(代わりに関数名が包まれてます)
ほかは大丈夫でしょう。
コンストラクタ、デストラクタの分離
CONTRACT_CLASS( // Class declaration (with contracts). template( typename T, (T) Default ) class (natural) ) { ... CONTRACT_CONSTRUCTOR_TPL( public explicit (natural) ( (T const&) value, default Default ) precondition( value >= 0 ) postcondition( get() == value ) // Unfortunately, no member initializers when body deferred. ) ; // Deferred constructor body definition, use `;`. CONTRACT_DESTRUCTOR_TPL( public (~natural) ( void ) ) ; // Deferred destructor body definition, use `;`. ... private: T value_; };
template< typename T, T Default > CONTRACT_CONSTRUCTOR_BODY((natural<T, Default>), natural) ( T const& value ) { value_ = value; } template< typename T, T Default > CONTRACT_DESTRUCTOR_BODY((natural<T, Default>), ~natural) ( void ) { // Do nothing. }
CONTRACT_CONSTRUCTOR_BODYマクロ、CONTRACT_DESTRUCTOR_BODYマクロがちょっと特殊でキモいですね。
ちなみに、それぞれ下のような構文になっています。
CONTRACT_CONSTRUCTOR_BODY(class_type, constructor_name) CONTRACT_DESTRUCTOR_BODY(class_type, destructor_name)
templateなクラスですが、CONTRACT_CONSTRUCTOR_BODY_TPLはないので気をつけましょう。
class_typeがtemplateであれば、それも一緒に記述します。記述の仕方が特殊でこれまたきもい。慣れるしかないです。間違えないように注意してください。
あ、class_typeは中括弧で囲んでくださいね!
これでctor,dtorの分離もできましたね。
Warning!!
コンストラクタの分離で落とし穴があります。ソースのコメントにも書いてありましたが、初期化子リスト(initialize( ... ))を使う場合、分離ができません。とても悲しいですが、今のところ対処法を知らないのでどなたか知っていれば教えてください。
メンバ関数の分離
CONTRACT_CLASS( // Class declaration (with contracts). template( typename T, (T) Default ) class (natural) ) { public: ... CONTRACT_FUNCTION_TPL( public bool (equal) ( (natural const&) right ) const postcondition( auto result = return, result == not less(*this, right) && not greater(*this, right) ) ) ; // Deferred member function body definition, use `;`. CONTRACT_FUNCTION_TPL( public (T) (get) ( void ) const ) { return value_; } private: T value_; };
template< typename T, T Default > bool natural<T, Default>::CONTRACT_MEMBER_BODY(equal) ( natural const& right ) const { return not less(*this, right) && not greater(*this, right); }
CONTRACT_MEMBER_BODYマクロを用いて実現します。
CONTRACT_MEMBER_BODY(function_name)
書き方に関してはこう決まってるから、としか言いようがなく、残念です。書き方に慣れてください。
一つ一つ見ていけば大したことはなかったですね。
演算子オーバーロード
演算子オーバーロードもできます。
CONTRACT_CLASS( class (Data) ) { CONTRACT_CLASS_INVARIANT( data_ >= 0 ) public: CONTRACT_CONSTRUCTOR( public (Data) ( const int data ) precondition( data <= 0 ) postcondition( data <= 0 ) initialize( data_( data ) ) ) { } CONTRACT_FUNCTION( public bool operator(==)(equal) ( (const Data&) right ) const ) { return data_ == right.data_; } public: int data_; };
演算子オーバーロードはCONTRACT_FUNCTIONマクロ、(分離するならCONTRACT_FREE_FUNCTION, CONTRACT_MEMBER_FUNCTION)を使います。演算子オーバーロードは他とチョット書き方が違います。
operator(symbol)(arbitrary-alphanumeric-name)
arbitrary-alphanumeric-nameの部分に任意の英数字を入れます。ここでは==だったのでequalと名づけましたが、好きにしていいんじゃないでしょうか。
例外処理
例外処理は普段通り使えますよー。※だだしコンストラクタの初期化子以外に限る
例外が投げられた後のpostcondition等の扱いはその1を参照ください。
CONTRACT_CLASS( class (Data) ) { CONTRACT_CLASS_INVARIANT( data_ >= 0 ) public: ... CONTRACT_FUNCTION( public void (func) () ) { throw std::runtime_error( "Hoge" ); } public: int data_; }; int main() { Data data( 10 ); try { data.func(); } catch( const std::exception& e ) { std::cout << e.what() << std::endl; } }
コンストラクタの初期化子は特別な書き方をします。下のコードを見てください。
CONTRACT_CLASS( template( typename T ) class (array) ) { CONTRACT_CLASS_INVARIANT_TPL( size() >= 0 ) public: struct out_of_memory {}; public: struct error {}; CONTRACT_CONSTRUCTOR_TPL( public explicit (array) ( size_t count ) precondition( count >= 0 ) postcondition( size() == count ) try initialize( data_(new T[count]), size_(count) ) catch(std::bad_alloc&) ( throw out_of_memory(); ) catch(...) ( throw error(); ) ) {} CONTRACT_DESTRUCTOR_TPL( public virtual (~array) ( void ) throw( ) // 例外投げない ) { delete[] data_; } // ...
initializeの部分が try で囲まれていますね。初期化子リストで例外が投げられると、その下のcatchブロックに投げられて、対応したところが呼ばれます。catch( ... ) と書くこともできますよ。
あっ、もういちいちWarningで赤く出さないですが、catchかける最大数も決まってるので…
CONTRACT_LIMIT_CONSTRUCTOR_TRY_BLOCK_CATCHESマクロをご確認ください(長い)。デフォルトは10です。
普段メンバ関数にthrow()とつけてる方は、上記のようにCONTRACT_DESTRUCTOR_TPLマクロの中でやってることをやってください。
Static Assertions
static_assertも使えます!
CONTRACT_FUNCTION( template( typename To, typename From ) (To*) (memcopy) ( (To*) to, (From*) from ) precondition( static_assert(sizeof(To) >= sizeof(From), "destination too small"), static_assert((boost::is_convertible<From, To>::value), "incompatible types"), to, // ぬるぽちぇっく from // ぬるぽちぇっく ) ) ;
static_assertはpreconditionに書こうがpostconditionに書こうがコンパイル時にチェックされます。
Constant Assertions
int foo = 0; CONTRACT_FUNCTION( int (func) ( int x ) ... postcondition( auto result = return, result = 10, // エラー!resultは自動的にconstになる foo = 10, // エラー…にならなーい! ) ) { ... }
グローバル変数 foo が存在しています。よく見てください、postconditionで代入を行なっていますね。
resultはライブラリが自動的にconstにしてくれます(他にもメンバ変数、引数、戻り値、事前条件又は事後条件で宣言した変数etc)。
しかし、fooは代入できてしまって、実際これ呼び出した後fooの値を確認すると10になってます。副作用を伴っててだめですね。
なので、少しでも事故を少なくするために構文が用意されてます。
int foo = 0; CONTRACT_FUNCTION( int (func) ( int x ) ... postcondition( auto result = return, const( foo ) foo = 10 // エラー! ) ) { ... }
const( variable1, variable2, ... )
書き方はこんな感じです。多分殆ど使わないと思います。(だってたいていは自動的にconstですからね
Select Assertions
その1でこんなの見かけませんでした?
CONTRACT_FUNCTION( public virtual void (add) ( const int value ) precondition( std::find( std::begin( v_ ), std::end( v_ ), value ) == std::end( v_ ) ) postcondition( auto old_size = CONTRACT_OLDOF v_.size(), auto old_found = CONTRACT_OLDOF std::find( std::begin( v_ ), std::end( v_ ), value ) != std::end( v_ ), old_found ? true : std::find( std::begin( v_ ), std::end( v_ ), value ) != std::end( v_ ), old_found ? true : v_.size() == old_size + 1 ) ) { v_.push_back( value ); }
old_found ? true : std::find( std::begin( v_ ), std::end( v_ ), value ) != std::end( v_ ), old_found ? true : v_.size() == old_size + 1
この部分、もっと複雑になったら二項演算子じゃ厳しいですよね?じゃあどうするのか。
if文を使います!!(え
CONTRACT_FUNCTION( public virtual void (add) ( const int value ) ... postcondition( auto old_size = CONTRACT_OLDOF v_.size(), auto old_found = CONTRACT_OLDOF std::find( std::begin( v_ ), std::end( v_ ), value ) != std::end( v_ ), if( old_found ) ( true ) else ( std::find( std::begin( v_ ), std::end( v_ ), value ) != std::end( v_ ) ), if( old_found ) ( true ) else ( v_.size() == old_size + 1 ) ) ) { v_.push_back( value ); }
ifの構文は
if(boolean-condition) ( // Round parenthesis `()`. ... // Comma-separated assertions. ) else ( // Else optional as usual. ... // Comma-separated assertions. )
です。なかでどんな処理してんだこれ…
Conceptとの併用
Conceptとも相性バツグンです。
concept_checkを使う
#include <boost/concept_check.hpp> CONTRACT_CLASS( template( typename T ) requires( boost::CopyConstructible<T> ) class (vector) ) { CONTRACT_CLASS_INVARIANT_TPL( size() <= capacity(), capacity() <= max_size() ) { ... } ...
基本構文を下に書きました。
template( template-parameter1, template-parameter2, ... ) requires( concept1, concept2, ... )
ここでは基本的なconceptであるboostのCopyConstructibleを使っています。もしTがCopyConstructibleでない場合、concept_checkによってコンパイルエラーになります。
将来的な話
将来的には下のようにContractのコンセプトがかけるように考えられているそうな。
C++1yにはconceptはおそらく入ると思いますし、楽しみですね!!
CONTRACT_CONCEPT( auto concept (LessThanComparable) ( typename T ) ( bool operator(<)(less) ( T , T ) ) ) CONTRACT_CONCEPT( concept (InputIterator) ( typename Iterator, typename Value ) ( requires Regular<Iterator>, (Value) operator(*)(deref) ( Iterator const& ), (Iterator&) operator(++)(preinc) ( Iterator& ), (Iterator) operator(++)(postinc) ( Iterator&, int ) ) ) CONTRACT_CONCEPT( // Derived concepts. concept (ForwardIterator) ( typename Iterator, typename Value ) extends( InputIterator<Iterator, Value> ) ( // Other requirements here... ) ) CONTRACT_CONCEPT( // Concept maps can be templated. template( typename T ) concept_map (InputIterator) ( T* ) ( typedef (T&) value_type, typedef (T&) reference, typedef (T*) pointer, typedef ptrdiff_t difference_type ) ) CONTRACT_CONCEPT( concept (Semigroup) ( typename Op, typename T ) extends( CopyConstructible<T> ) ( (T) operator(())(call) ( Op, T, T ), axiom (Associativity) ( (Op) op, (T) x, (T) y, (T) z ) ( op(x, op(y, z)) == op(op(x, y), z); ) ) )
明解でわかりやすい
さすがにこれはキモい
実際開発で使うとき
Debugモードでならまだしも、実際リリースするときにこのままだといくらなんでも遅いです。ありえません。考えたほうがいいです。
そんな時、一発で無効にできるようにマクロが用意されています。
CONTRACT_CONFIG_NO_PRECONDITIONS CONTRACT_CONFIG_NO_POSTCONDITIONS CONTRACT_CONFIG_NO_CLASS_INVARIANTS CONTRACT_CONFIG_NO_BLOCK_INVARIANTS CONTRACT_CONFIG_NO_LOOP_VARIANTS
#defineで宣言すれば、それぞれ対応したものを無効に出来ます。コンパイルするときにオプションで付けてやれば簡単です。
TDDとContractの関係
TDDはオブジェクトの表明を保証するものではないです。あくまで開発手法。
Contractは、本当に仕様って感じだと思ってます。
だから、TDDをやるとしても契約プログラミングを意識することで、より強力なインターフェースをつくり上げることが出来ると考えてます。逆もまた然り。契約プログラミングを行うことで、必要なテスト、テストの記述漏れ防止etcという効果も期待できそうです。
とある人と少し議論になった時に出てきたのが、「テストで書くべきコト⊇契約」という構図です。
難しいですね…どうなんでしょうか。僕より詳しい方、ぜひコメントで教えてください。よろしくお願いします。
とりあえず
結論:両方やれ
まとめ
2日で詰め込んだらこんなことになってしまいました。あんまりまとまってなくて申し訳ない。
本当はもっと書きたかったんですが、記事書き始めが1日だったのでこれぐらいが限界でした。
ご要望があればどんどん追記していくつもりなので、コメントいただければ幸いです。
英語の翻訳も微妙なところあるかもしれないので、間違いに気づいた方いればそれもコメントにお願いします。
Boost.Contractについてですが、なかなかおもしろいんじゃないかなと個人的には思ってます。今度実際の開発で試しに使ってみます。テストと組み合わせればより有効な手法だと思いますね。契約プログラミングかテストかどっちすればいいですか、って聞かれたら、可能な範囲でどっちもすればいいんじゃ?と答えると思います。コードはちょっときもいですが…w
ケースバイケースですよね。Contractは複雑な契約はちょっと苦手ですし。そうなると防御的プログラミングとかも検討することになるとおもいます。
辛かったこと書きます。
実際開発で使ってるのがVSなので、VS2012でコード打ってたんですが、インテリセンスが死ぬわ死ぬわで大変でした。全く意味なかったし…(途中から追い切れなくなって赤い波線が出る始末。もちろんコンパイルは通る)。
それが辛かったことですかね。
勉強になりました。おつかれさまです。