設定ファイルからの読み込みをもうちょっと C++ っぽくしてみる。

前のエントリ設定ファイルから true か false を読み出す。 - meg_nakagamiの日記ですけど、これは C++ 的にはまだまだ甘いので、みんながある程度 C++ を詳しく知っている、もしくは向上心があって知らないことを学習の機会とみなしてくれる人であるという恵まれたプロジェクトであったらどんなものを作るか考えてみます。

いろんな前提が考えられるかと思うのですが、

  • 複合的なデータ構造は格納しない、 key = valueスカラー値のみを扱う。
  • keyは殆どの場合予め定められたものを使用する、プログラムでキーを生成して格納するという用途でも使用できるべきだが、そのような用法は優先しない。
  • 保存は SetProperty、取得は GetProperty で文字列を扱う仕組みがすでにできている。
  • SetProperty、GetProperty はあらゆる文字列を扱うことができるようになっている。(エスケープなどはここでは考えない)
  • 同期については考えない。

ぐらいの前提があるものとします。
まず、

    bool Property::GetBool();

これは(ごく平均的なプログラマにとって)非常にわかりやすいのですが、ちょっと拡張性がよろしくないです。あらゆる型を受け取ろうとすると、無限に関数を書かなくてはいけません。
ということで、それを解決しようとするとこうなります。

// プロパティの文字列との相互変換
template <class T>
class PropertyTraits{
public:
    static boost::optional<T> Parse( const std::string& source ){
        try{
            return boost::lexical_cast<T>( source );
        }catch( boost::bad_lexical_cast &e ){
            return boost::none;
        }
    }
    static std::string Format( const T& t){
        return  boost::lexical_cast<std::string>( t );
    }
};

// プロパティで使用される bool 値の文字列との相互変換
template<>
class PropertyTraits<bool>{
public:
    static boost::optional<bool> Parse( const std::string& source ){
        std::string s = Text::Trim(source);
        if( Text::EqualsIgnoreCase(s, "true")) return true;
        if( Text::EqualsIgnoreCase(s, "false")) return false;
        return boost::none; 
    }
    static std::string Format( bool t){
        return  t ? "true" : "false";
    }
};

// プロパティ
class Property{
public:
    template<class T>
    static boost::optional<T> GetValue( const char* name ){
        return PropertyTraits<T>::Parse( GetProperty(name));
    }
    template<class T> 
    static void SetValue( const char* name, const T& t){
        SetProperty( name, PropertyTraits<T>::Format(t).c_str());
    }
};

これで string への入出力のある型であれば何でも出し入れでき、かつ boost::lexical_cast の挙動が嫌であれば PropertyTraits を特殊化して対応可能になりました。true/True/TRUE 問題はこれで解決です。
出す方は失敗するかもしれないので boost::optional で受けます*1。 FASLE(FALSEのまちがい)問題もこれで対応可能です。
ですが、各機能を実装したいプログラマとしては『設定ファイルがおかしいかもしれないから optional で・・・』とか、考えたくもないことで、『何も考えずにキーを渡したら値が返ってくる』という使用方法をしたいはずです。
これを解決するのが、デフォルト値の問題です。
で、ちょっと視野を広げて、そういう『何も考えずにキーを渡したら値が返ってくる』というものが欲しいプログラマが、キーを文字列で直接指定するのは(スペリングミスとかもあるし)あまり良さそうなことではないです。そういう人には予め何らかのキー定数を用意してもらうべきでしょう。で、そのような定数があるのなら、その定数の中にデフォルト値の情報をぶち込んでしまうこともできそうです。また、同じキーを別の型で取り出す、ということはアプリケーションの実装であまりありそうなことではないので、キー定数にはそのキーの値がどんな型で取り出されるべきかもついでに含ませてしまいましょう。
これを template PropertyKey; とすると、デフォルト値は T で持たせてあげたいのですが、このキーはグローバル変数になってしまうため、含まれる型がわからないと初期化がよくわからないことになってしまいそうです。ですので、 key, デフォルト値とも const char* で持つこととして、static リンケージにしてしまいましょう。デフォルト値を文字列で持ってしまうとお目当ての型に変換できない時わけがわからなくなりますが、それはあからさまなコーディングミスなので無視することとします。
実行時効率としてはいろいろ妥協してますが、この選択によってキーを追加するときは一行追加するだけですむことになりました、また、グローバルオブジェクトの初期化で使用されたとしても問題なくなりました。prop_key.h とかプロジェクト全体から見える位置にそれを置くことにしましょう。
(この選択は『デフォルト値を変えたら関連するコンパイル単位全部コンパイルしなおし』ということを受け入れるという選択でもあります。このプロパティ機構に依存するコンパイル単位がどれだけあるか、『デフォルト値やキー名だけを開発中ちょっと変えたい』という要求がどれだけあるか、などなど、プロジェクトの要求によっては extern にして、宣言も一箇所にまとめずそれぞれのモジュールで使いたい分だけそれぞれ宣言するようにしたほうがいいかもしれません。)

とすると、

// プロパティのキー
template<class T>
class PropKey : boost::noncopyable{
public:
    typedef T value_type;
    const char* m_Name;
    const char* m_Default;

    PropKey( const char* pName, const char* pDef) : m_Name(pName), m_Default(pDef){}
};

こういう型ができて、

class Property{
public:
    template<class T>
    static boost::optional<T> GetValue(const PropKey<T>& key ){
        return GetValue( key.m_Name );
    }

    template<class T>
    static T Get( const PropKey<T>& key ){
        std::string s = GetProperty(key.m_Name);
        boost::optional<T> v = PropertyTraits<T>::Parse( s );
        if( ! v ){
            // if( ! s.empty()){
            //   LOG_WARN( "%s が予期しない値なためデフォルト値 %s を使用します。", key.m_Name, key.m_Default );
            // }
            v = PropertyTraits<T>::Parse( key.m_Default );
            
            if( ! v ){
                // LOG_ERROR( "プロパティ %s が不正です", key.m_Name );
                throw std::logic_error("bad property value.");
            }
        }   
        return *v;
    }

    template<class T>
    void Set( const PropKey<T>& key, const T& v ){
        SetProperty(name,  PropertyTraits<T>::Format(t).c_str());
    }
}

こういう関数を Property に追加して、(GetValue() はオーバーロードですが)
で、prop_key.h みたいなファイルに

namespace the_app{
    namespace prop{
        const PropKey<int>         HttpPort       ("HttpPort",         "80");
        const PropKey<std::string> HttpHost       ("HttpHost",         "localhost");
        const PropKey<bool>        EnableExtention("EnableExtention",  "false");
        
        // 以後いろいろ追加
    }
}

こんな感じでキーの実体を並べると

namespace prop = the_app::prop;

void func(){
    
    // これでとにかく値が取ってこれる、
    // 設定値がおかしくてもデフォルト値 false がくる。何も考えなくていい。
    bool to_enable = Property::Get( prop::EnableExtention );
    
    // 値がおかしい時にデフォルト値じゃなく細やかに処理したければ
    optional<bool> x = Property::GetValue( prop::EnableExtention );
    // if( !x ){
    //    
    // }
    
    // キーが値は string,int であることを知っているのでこれでおk。
    // 最も簡潔。
    string host = Property::Get( prop::HttpHost );
    int port = Property::Get( prop::HttpPort );
    
    
    // 『いや、俺は port を string でとってきて欲しいのだ』←こいつは変なやつだからちょっと複雑な記述が必要。
    optional<string> str_port = Property::GetValue<string>( prop::HttpPort.m_Name );
    string s = *str_port;
}

こんな感じで使えるようになります。

もうちょっと洗練することはできそうですが、何が最良かはプロジェクトによって異なるでしょうからこれ以上はなんとも言えないですね。

*1:悲しい話なんですが、現実的なプロジェクトにおいてはこれは『メンバーに boost::optional という新しい概念についての学習を強いる』選択であり、メンバーの学習のオーバーヘッドが大きく、ちょっとそれについて考慮する必要が発生します。が、ここでは考えないとします。