進捗の存在しない世界

技術的なこととかそうじゃなさそうなこととかをアウトプットして記憶を定着させたい

明日から使えるC++イディオム

C++初心者アドベントカレンダー13日目の記事です。

いくつかC++のイディオムを書いていきます。

スマートポインタ

スマートポインタ(Smart Pointer)はその名の通り、賢いポインタです。一般的にメモリ確保をするプログラムは以下のようになります。

int * pointer = new int;

しかし、これでは自分で確保した領域を解放しないといけないので面倒ですし、人間は借りたものは返さず忘れてしまう生き物なので、解放を忘れてしまうこともあります。

スマートポインタは、そんな人類の代わりに自動でいらなくなったメモリ領域を解放してくれたりしてくれます。スマートポインタは標準ライブラリの実装があって、std::unique_ptr、std::shared_ptr、std::weak_ptrになります。 std::shared_ptrは以下のように使えます。

#include <memory>
#include <iostream>

int main()
{
    std::shared_ptr<int> hoge = std::make_shared<int>(42); //make_sharedはshared_ptrを作成するための関数
    //C++14以前だと make_sharedがないのでnewする
    // std::shared_ptr<int> hoge(new int(42));
    // あとはポインタと同じように使える
    std::cout << *hoge << std::endl; // 42

    {
        std::shared_ptr<int> fuga = std::make_shared<int>(1);
        
    } // ブロック内で宣言したfugaの寿命はここまで、ポインタは解放される

    {
        std::shared_ptr<int> piyo = hoge; // hogeとポインタを共有する
    } // ここでpiyoの寿命は尽きるが、ポインタはhogeがまだ持っているので解放されない
   
    return 0; //ここでhogeの寿命が尽きて、ポインタも解放される
}

各ポインタの使い分け方は本記事の範疇を超えるので別記事に譲るものとしますが、簡単に説明すると、std::unique_ptrが他のスマートポインタと中身のポインタを共有できないもの、std::weak_ptrが、自分がポインタを持っていても他のshared_ptrの寿命が尽きたら自分が持っているポインタが解放されてしまうもの、といった感じです。

Pimpl

Pimpl(Pointer to IMPLementation)は、実装を隠したり、ヘッダを書き換えないことによってコンパイルを高速化したりするのに使われます。C++17のmoduleで必要がなくなると思いますが、最新のC++が使える職場はエウロパに知的生命体が生まれる確率くらいでしか存在しないでしょうから、まだ役に立つと思います。

以下のようなクラスがあるとします。

// hoge.h

class Hoge {
    int foo_;
    void baz();
public:
    int bar();
    Hoge() = default;
    ~Hoge() = default;
};
// hoge.cpp
#include "hoge.h"
void Hoge::baz()
{
    // 処理
}
int Hoge::bar()
{
    // 処理
    baz();
}

このクラスで新しくpiyoという変数を追加したいとすると、それがprivateな変数でもヘッダを書き換えないといけません。 ヘッダを書き換えないといけないということは、それを読み込んでいるソースファイルをもう一度コンパイルしないといけないということです。1つや2つなら良いのですが、10とか20とか100とかになるとバカにできるコストではありません。

そこで以下のようにするとそんなコストが無くなります。

// hoge.h
#include <memory>
class Hoge {
    class HogeImpl; //前方宣言のみ行う
    std::unique_ptr<HogeImpl> impl_; // このクラスでしか使わないのでunique_ptrで
public:
    Hoge(); 
    ~Hoge(); // pimplの場合ここにデストラクタは書けない。詳しくは下で
    int baz();
};
// hoge.cpp
#include <memory>
#include "hoge.h"
class Hoge::HogeImpl
{
    int foo_;
    void baz() { // 処理 }
public:
    int bar() { baz(); // 処理 }
};
Hoge::Hoge() : impl_(std::make_unique<HogeImpl>())
{
}
Hoge::~Hoge() = default;
int Hoge::bar()
{
    return impl_>bar();
}

こんな感じになります。デストラクタをヘッダに書けないのは、Hogenのデストラクタでunique_ptrがHogeImplのデストラクタを呼びたいけど、不完全なクラスであるHogeImplのデストラクタを呼べないからです。なので、ソースファイルで定義されてからでないとHogeのデストラクタが書けません。

pimplを使うと、bazの挙動を変えるために適当なprivate変数を追加したいとかいうときにヘッダファイルを書き換える必要がありません。以下のように書けばOK。

// 省略
class Hoge::HogeImpl
{
    int foo_;
    int qux_;
    void baz() { qux_ = 1; //処理}
public:
    int bar() { baz(); // 処理}
};
// 略

pimplの弱点も幾つかあって、例えば継承を行いにくいとか、元のコードより(若干)速度が落ちるなどがあります。用法用量を守って使っていきましょう。

間違っている点とか気に入らない点があったらコメントください。

参考文献