「C++のためのAPIデザイン」読んだ+ちょっとした補足

C++のためのAPIデザイン

C++のためのAPIデザイン

どんな本か

この本は、誰かに使ってもらうためのソフトウェアを(主にC++で)書く際に役立つ本です。APIというと外部ユーザーに使ってもらうためのソフトウェアインターフェースだけを指すようにも感じられますが、この本で言うAPIとはもっと広い意味で、

自分が書いたコードを使う全ての人に対してコードの操作方法を定義するヘッダ、ライブラリ、ドキュメント

を指しています。また、扱う内容もAPIの設計原則からアーキテクチャ設計手法、スタイル、実装テクニック、パフォーマンス、バージョン管理、自動テスト等、設計〜保守に至るまで多岐に渡っています。

APIの善し悪しに関しては私自身感覚的な判断でやってきた部分が結構あるので、拠り所となる日本語の書籍が1冊あると、社内でのコードレビューとか新人の教育にも役立ってくれそうな予感がします。類書として「大規模C++ソフトウェアデザイン」もありますが、どちらかを選ぶのであれば内容の広範さと日本語としての読みやすさ、内容の新しさから本書を推したいです。

どんな人におすすめか

自分のコードを使うのは並行開発する同僚だったり、未来の自分だったりもするので、ライブラリの作成者だけではなく、大規模プロジェクトでC++を使おうとする人にはぜひ読んでほしいなあと思える内容です。なお、C++に限らない記述も多いため、非C++erでも役に立つのではと思いますが、基本的C++の文法を知っていないと読みにくいかもしれません。

補足したい所

内容は筆者の開発経験とC++の知識に基づいていてとても勉強になるのですが、重箱の隅的な部分でいくつか補足しておきたい部分があるので自分用メモを兼ねて書いておきます。

STLアルゴリズムの選択はコンテナには依存しない(2.4.4, p.56)」は厳密ではない

たとえばstd::sortはstd::vectorやstd::dequeには使えますが、std::listには使えません。コンテナごとにイテレータカテゴリが違うため、アルゴリズムが要求するイテレータの要件を満たさない場合はコンパイルが通りません。

また、p.52では「異なるSTLコンテナが同名のメンバ関数を持つため、あるコンテナの使い方が分かれば別のコンテナの使い方も分かる」旨の解説がありますが、これも厳密ではないかなと感じました(使い方が分かる、のレベルとしてどれ位を想定するのか次第ですが)。例えシグネチャが同じメソッドでも、コンテナによって例外安全に対する保証が異なる、計算量が異なる、イテレータの無効化タイミングが異なるといった機能面での差があるということは認識しておいたほうが良いと思います。詳しくはEffective STL 第2項「コンテナに依存しないコードという幻想に注意しよう」あたり。

RAIIが必要である理由は、returnステートメントだけではない(2.4.5, p.60)

リソースの確保・解放はコンストラクタとデストラクタでやるべきというのはC++としては当たり前の話ではあります。本書ではその例として、以下のコードが挙げられていました。

void SetName(const std::string& name)
{
    mMutex.lock();
    if (name.empty()) {
        return;
    }
    mName = name;
    mMutex.unlock();
}

説明の流れとしては、このコードはreturn時のunlockが抜けているのでデッドロックが起きる→RAIIを使えばreturnステートメントを1つずつチェックする必要がなくなる、みたいな感じだったのですが、実際にはmName = nameの代入演算で例外を送出する*1という関数の抜け方があるため、return文だけのチェックでも十分ではありません。それを考えると、RAIIを使わずにリソースリークを起こさないコードを正確に書くのは、より困難になります。

void SetName(const std::string& name)
{
    try {
        mMutex.lock();
        if (name.empty()) {
            mMutex.unlock();
            return;
        }
        mName = name;
        mMutex.unlock();
    } catch (...) {
        mMutex.unlock();
        throw;
    }
}

結論は一緒ですが、理由としては複数return文の場合に困るというより、例外に備えたリソース確保を書くのが大変というほうが大きいように思います。

コンパイラが暗黙に生成するメソッドは4つ(6.2, p.209)」とは限らない

C++11であればムーブコンストラクタとムーブ代入演算子も加えた6つのメソッドが暗黙に生成されます。C++11環境でのクラス定義の方針は、以下が参考になります。

C++11時代におけるクラスの書き方 | イグトランスの頭の中(のかけら)

STL文字列の実装のほとんどはCoW(7.7, p.278)」は2013年現在では正しくない

以前はそうだったのですが、現時点では少なくともVC, clangではstd::stringはCoW(書き込み時コピー)を使っていません。さらに、C++11では規格上CoWでのstring実装ができないような文面になっています。

参考:
c++ - Legality of COW std::string implementation in C++11 - Stack Overflow
C++03 と C++11 の互換性 - melpon日記 - HaskellもC++もまともに扱えないへたれのページ

gccでは4.8時点でまだCoWが残っているようですが、C++11との整合性のためにいずれ消えていくはずです。CoWが使われない方向になっているのはマルチスレッド環境に対応するために必要な排他制御のコストがCoWのメリットを上回るからで(規格上禁止された経緯に関しては追ってないので分からない…)、自分のクラスでCoWをやろうとする場合も、他の最適化が使えないか*2検討した方が良いかと思います。CoWに関しては自分の知る限り「More Exceptional C++」が詳しいです。

例外安全はAPI設計に影響する

特に本書ではどこにも触れられていなかったのですが、C++APIを設計するに当たって、例外を使うことにした場合には「例外安全性はAPI設計に影響する」ことを考慮に入れなければなりません。本書では6章で以下のインターフェースを持つStack型が出てきます。

template <typename T>
class Stack
{
public:
    void Push(T val);
    T Pop();
    bool IsEmpty() const;
private:
    std::vector<T> mStack;
}

このPop()はどう実装しても例外安全の「強い保証」ができません*32013/5/15追記:Tの型とコンパイラの最適化次第では強い保証が満たせる状況が有る、との指摘を頂きました。コメント欄参照。例外安全にしっかりケアしたいのであれば、std::queueのようにPop()とTop()に分離するとか、API設計時点で配慮しておく必要が出てきます。

*1:std::length_errorまたはstd::bad_alloc

*2:例えばVCやclang(libc++)では、CoWの代わりの速度最適化として、内部で小サイズの配列バッファを持たせており、短い文字列の構築や結合時にアロケーションを行わないようになっている

*3:詳しくは全人類が読むべき本「Exceptional C++」へ