クラウドエンジニアのノート

情報技術系全般,自分用メモを公開してます。

c++でpimplイディオムの書き方

はじめに

私は基本的にヘッダオンリーでC++のソースを書くタイプなのですが、研究で使うRCカーの制御プログラムが肥大化してきて、フルビルドに10分近くかかるようになってしまいました。そのまま実装とヘッダに分けても良いのですが、どうせならと思ってPimplイディオムを導入しました。

以下の記事を参考にさせて頂きました。 qiita.com

メリット・デメリット

Pimpl とは “Pointer to Implementation"から来ているみたいですね。

メリット

  • private なものを隠すことができる
  • コンパイル時間が短くなる

デメリット

  • ソースが肥大化する
  • 無駄にメモリ確保が必要

アンチパターンなんて記事も見るので、良い悪いは人それぞれだとは思いますが、個人的にprivateなものがヘッダからは見えず、ソースが読みやすくなることと、privateな部分での変更をいちいちヘッダにも反映させなくて良いのが気に入ってます。

使い方

ヘッダオンリー例

手順どおりにやれば至ってシンプルです。
まず、以下のようなソースを例に説明します。

test.h

#ifndef TEST_H_
#define TEST_H_

#include <iostream>

class Test {
   private:
    int pri_value;

    void twice() { pri_value *= 2; }
    void print() { std::cout << "pri_value: " << pri_value << std::endl; }

   public:
    int pub_value;

    Test() : pri_value(2), pub_value(4) { std::cout << "constructor" << std::endl; }
    ~Test() { std::cout << "destructor" << std::endl; }

    void process() {
        twice();
        print();
    }
    int process2(const int v_) { return pub_value * v_; }
};

#endif  // TEST_H_

main.cpp

#include "test.h"

int main(void){
    Test test;
    test.process();
    std::cout << "result of process2: " << test.process2(2) << std::endl;
    
    test.pub_value = 100;
    std::cout << "result of process2: " << test.process2(4) << std::endl;
}

実行例

$ ./a.out
constructor
pri_value: 4
result of process2: 8
result of process2: 400
destructor

手順

  1. test.hのprivateにTestImpl(クラス名+Impl)の前方宣言と、そのクラスへのポインター(pimpl)を設置
  2. test.cppを作成し、TestImplクラスを宣言
  3. test.hのprivate, publicの中身(変数、関数、コンストラクタ…)を新しいクラスに移動
  4. Testクラスのpublic変数を参照に変更
  5. test.cpp の、TestクラスコンストラクタでTestImplをnewして、参照で定義したpublic変数をpimplの保持する実体へ繋ぐ
  6. test.cppにTestクラスのpublic関数の実装をpimplの関数の実体へと繋ぐ

書き換え後

test.h

#ifndef TEST_H_
#define TEST_H_

#include <iostream>
#include <memory>
#include <vector>

class Test {
   private:
    class TestImpl;
    std::unique_ptr<TestImpl> pimpl;

   public:
    int& pub_value;

    Test();
    ~Test();

    void process();
    int process2(const int);
};

#endif  // TEST_H_

test.cpp

#include "test.h"

class Test::TestImpl {
   private:
    int pri_value;

    void twice() { pri_value *= 2; }
    void print() { std::cout << "pri_value: " << pri_value << std::endl; }

   public:
    int pub_value;
    TestImpl() : pri_value(2), pub_value(4) { std::cout << "constructor" << std::endl; }
    ~TestImpl() { std::cout << "destructor" << std::endl; }
    void process() {
        twice();
        print();
    }
    int process2(const int v_) { return pub_value * v_; }
};

Test::Test() : pimpl(new TestImpl()), pub_value(pimpl->pub_value) {}
Test::~Test() {}

void Test::process() { pimpl->process(); }

int Test::process2(const int v_) { return pimpl->process2(v_); }

main.cppは同じ

実行例

$ ./a.out
constructor
pri_value: 4
result of process2: 8
result of process2: 400
destructor

コメント

  • unique_ptrにすることでポインタ管理の心配いらず
  • 関数を一度無駄に経由することになるので、コピーコストラクタ走るのが嫌な時は転送すれば良い(コピーよりはまし)

まとめ

確かにソースは肥大化しますが、こっちのほうが1つのファイルで何やってるかわかるので、読みやすいような気がします。