Rokiのチラ裏

学生による学習のログ

読書感想 #1-1 - STL Tutorial and Reference Guide,Second Edition C++ Programming with the Standard Template Library

一度全て通して読み終わっている書籍なのだが、こういった標準仕様に関わる書籍は何度読んでも私は個人的に楽しいので、なんとなく、今一度通しで読んでいる。

STL―標準テンプレートライブラリによるC++プログラミング 第2版

STL―標準テンプレートライブラリによるC++プログラミング 第2版

  • 作者: ディビッド・R.マッサー,アトゥルサイニ,ギルマー・J.ダージ,David R. Musser,Atul Saini,Gillmer J. Derge,滝沢徹,牧野祐子
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2001/12
  • メディア: 単行本
  • 購入: 5人 クリック: 74回
  • この商品を含むブログ (18件) を見る
折角なので、各文書に対する個人的な感想文を、つらつらと書いていこうと思う。
尚、引用部分を除く各章ごとのサンプルコードは、本書で述べられている内容を基準に私が記述したコードである。またサンプルコードの中では、私が独自的に定義したコード*1を用いている部分がある。コードを確認するには、脚注を参照とのこと。

  • 「ステバノフの本書第二版への序文」では筆者のSTLジェネリックプログラミングへの、主なコンセプトや理念が書かれている。
  • 「ステバノフの本書第二版への序文」xi~xii
    ---大域汎用アルゴリズムの使い方だけでなく、もっと重要なことは、オブジェクトを部分として所有することとオブジェクトをポイントすること、これら二つの概念をはっきり分けている事実において。次のような場合、
    T a = b;
    
    これは単に同一オブジェクトへのポインタがもう一つできるのではなく、すべての部分が別個に存在するようなオブジェクトのコピーを作成することであると想定している。
    例えば、C++では、operator overloadingが行えるので、assignment operatorをムーブな動作として定義できてしまう。(かの有名なauto_ptrなど)。これは、組み込みタイプライクなセマンティックスではないし、STLとしては動作の保障はしない。
    ...当たり前のことに思えるが、実際問題、組み込みタイプのoperatorとは掛け離れたセマンティックを持つ、operator overloadingをしているプロジェクトを私は見た事があるから、なんともそういった部分を実際に垣間見ると、なんでもできる事の反面に恐怖感を抱きかねない。
  • 「ステバノフの本書第二版への序文」xii
    STLが広範に使用されている反面、汎用コンポーネントのライブラリをたくさん作成するという私の希望はかなえられていない。作成されない理由を私なりに判断すると、作業を支援する財政上の仕組みがないからである。基本アルゴリズムで金は儲からない。少人数のコンポーネント職人チームによって業界全体のためにデザインされなければならない。私は幸運にもコンピュータ大企業からSTL作業の資金を2度受けたが、資金を得る確実な方法がない限り本格的な作業には着手できない。米国政府あるいは欧州連合が、汎用ソフトウェアコンポーネント作成に専念する小規模でも効果的な団体を財政支援してほしいものだ。それは研究ではなく、あくまでも適切に整理・ドキュメントされた効率的汎用コンポーネントの実際の製造でもある。
    この後に、「読者も議員宛に要望書で訴えてほしい。」とある。日本国内でも、なにか訴える手段があって、その手段が明白なのであれば、私は是非とも訴えてもみたいものだ。私は、それほど、この意見に全面賛成である。以前私が、某ロシアの少年*2の如く書き殴った、就職や就活についての駄文を当ブログにアップロードしたが、私の求めている理想的な"仕事"というのは恐らくこういった「汎用ソフトウェアコンポーネント」を製造する団体でのクリエイティブな開発の事なのかもしれない。。。
  • 1.1~1.3.6
    本書の対象読者とジェネリックプログラミングの重要性、またその具体的な実現法が書かれている。 またクラステンプレート、関数テンプレート、メンバ関数テンプレートなどの使用用途が書かれている。それらを利用した汎用性と効率性の両立に基づいた設計思想を実際のコードに起こす際の具体的な方法が書かれている。
  • 1.4~1.5.3
    STLアルゴリズムのパフォーマンス保証を理解するための表記法、またアルゴリズムごとに適した観点は変動する事を示している。
  • 1.5.3
    プログラムのパフォーマンステストは経験に基づいて行う事が賢明で、それもできるだけ現実に近づけて、実用段階での環境やデータがそろった状況で行うことが望ましい。
    賛同できる内容である。
  • 2.1.1
    各列コンテナの紹介。
    まず各アルゴリズムに対して指定する末尾、つまり各コンテナのメンバ関数版endと非メンバ関数版endが何を指しているのか、指すべきなのかを明示的に示している。 また本書では、以下の関数を様々なページのサンプルコード中に用いるようだ。(本書に記載されているコードを現代風に、またヘッダーとして扱えるよう一部変更、追記した)
    #ifndef INCLUDED_SML_MAKE_HPP
    #define INCLUDED_SML_MAKE_HPP
    #include<cstring>
    namespace sml{
    template<class Container>
    constexpr Container make(const char s[]){return Container(s,&s[std::strlen(s)]);}
    }
    #endif
    
    正直std::stringで良いような気がしないでもないが、サンプルコードとして説明するにあたっての事だろうと解釈している。以下のように使う。(本書の例2.3を参照して独自的に解釈し記述したコードである。)
    #include"sml/make.hpp"
    #include<iostream>
    #include<cassert>
    #include<list>
    #include<algorithm>
    
    int main()
    {
        std::list<char> list(sml::make<std::list<char>>("mark twain"));
        std::reverse(std::begin(list),std::end(list));
        assert(list==sml::make<std::list<char>>("niawt kram"));
    }
    
  • 2.1.2
    順序連想コンテナの紹介。std::map<std::string,long>を例に使用用途を紹介している。
  • 2.2.1
    汎用アルゴリズムの一例としてstd::findをあげている。例2.5、2.6、2.7、2.8のコードでは以下のように、汎用アルゴリズムのインタフェースの統一性、汎用性を示している。またその一方、渡されたコンテナタイプによってアルゴリズムの処理法と効率性が異なる事について触れている。(以下は例2.5、2.6、2.7、2.8を参照して独自的に解釈し記述したコードである。)
    #include"sml/make.hpp"
    #include<cassert>
    #include<vector>
    #include<list>
    #include<deque>
    #include<algorithm>
    #include<cstring>
    
    int main()
    {
        const char s[]="C++ is a better C";
        const std::vector<char> vec(sml::make<std::vector<char>>(s));
        const std::list<char> list(sml::make<std::list<char>>(s));
        const std::deque<char> deq(sml::make<std::deque<char>>(s));
        
        const char* const where=std::find(s,&s[std::strlen(s)],'e');
        const std::vector<char>::const_iterator where1=std::find(std::begin(vec),std::end(vec),'e');
        std::list<char>::const_iterator where2=std::find(std::begin(list),std::end(list),'e');
        const std::deque<char>::const_iterator where3=std::find(std::begin(deq),std::end(deq),'e');
    
        assert(*where=='e'&&*(where+1)=='t'&&
                *where1=='e'&&*(where1+1)=='t'&&
                *where2=='e'&&*(++where2)=='t'&&
                *where3=='e'&&*(where3+1)=='t');
    }
    
    また、汎用アルゴリズムの一例としてstd::mergeをあげている。二つの入力要素列と結果の要素列が異なるコンテナである場合でも動作すると、柔軟さを示している。
    以下は、例2.9、2.10を参照して独自的に解釈し記述したコードである。*3
    #include<numeric>
    #include<iostream>
    #include<list>
    #include<deque>
    #include<algorithm>
    #include<cassert>
    #include<cstring>
    
    #include"sml/make.hpp"
    #include<iostream>
    int main()
    {
        const char s[]="aeiou";
        
        std::list<char> list(26);
        std::deque<char> result;
    
        std::iota(std::begin(list),std::end(list),'a');
        std::copy(std::begin(list),std::end(list),std::back_inserter(result));
    
        list.remove_if([&s](const char c){
                for(auto b=std::begin(s); b!=std::end(s); ++b)if(*b==c)return true;
                return false;
                }); // a~zからs[]にあるchar文字(aeiou)を排除
    
        std::deque<char> deque(list.size());
        std::merge(s,&s[std::strlen(s)],std::begin(list),std::end(list),std::begin(deque));
        assert(deque==result);
    }
    
  • 2.3
    反復子によって、その反復子を名乗るべく必要とされる演算処理が示されている。
  • 2.4
    関数オブジェクトの有用性を示している。P38には、binary_functionが継承されたコードが記載されており、時代の相違を感じる。*4
  • 2.5
    アダプタの有用性を示している。reverse_iteratorなどのイテレータアダプタ、std::stackなどのコンテナアダプタ、std::not_fn、std::bind*5などの関数アダプタについて書かれている。
  • 2.6
    アローケータについて軽く触れている。本書では深く立ち入らないようだ。一応、デフォルトアロケータである、std::allocatorを用いた、サンプルコードを書いた。*6
    #include<memory>
    #include"../srook/srook/make_elements_string.hpp"
    #include<algorithm>
    #include<numeric>
    #include<iostream>
    
    template<class _Tp>
    class X final{
        std::allocator<_Tp> a;
        _Tp* ptr;
        const std::size_t _size=0;
    public:
        explicit X(const _Tp& size):ptr(a.allocate(size)),_size(size)
        {
            for(std::size_t i=0; i!=_size; ++i)a.construct(ptr+i);
        }
        void iota(const _Tp& value){std::iota(ptr,ptr+_size,value);}
        void output()const{std::cout<<srook::make_elements_string(ptr,ptr+_size)<<std::endl;}
        ~X()
        {
            for(std::size_t i=0; i!=_size; ++i)a.destroy(ptr+i);
            a.deallocate(ptr,_size);
        }
    };
    
    int main()
    {
        X<int> x(10);
        x.iota(0);
        x.output();
    }
    
    まあ通常、こういった使い方でわざわざstd::allocatorを用いる事はないだろう。

続く。

*1:https://github.com/falgon/SrookCppLibraries

*2:通称:アレマン・ポセイド

*3:std::list::size()は実装次第で線形時間的にアクセスして値を算出する場合があっても可笑しくはない。私個人としては、各ライブラリに実装依存するこのメンバ関数を用いる事に、好ましさを感じていないが、簡略化のため、今回は用いた。

*4:C++1zでunary_function、binary_functionは廃止される。(N4190) "unary_function/binary_function were useful helpers when C++98-era adaptors needed argument_type/etc. typedefs. Such typedefs are unnecessary given C++11's perfect forwarding, decltype, and so forth. (And they're inapplicable to overloaded/templated function call operators.)"

*5:本書が刷られた当初はstd::not1,2とstd::bind1st,2ndの事を指している。C++1zでこれらは廃止される。(上記N4190参照)

*6:"srook::make_elements_string"については、前エントリを参照とのこと。