C++でImmutableクラス

C++のクラスは、下記のように普通の書き方をするとmutableなものになります。

struct Person {
    std::string Name;
    int Age;
};

今回はこのクラスをImmutableなものにしてみます。

メンバーをconstに

下記のようにメンバーをconstにすると、コンストラクタの初期化構文でのみセットでき、代入でセットすることができなくなります。

struct Person {
    const std::string Name;
    const int Age;
    Person(std::string name, int age) : Name(name), Age(age) {}
};

Person p("Pieter", 10);
p.Age = 20; // NG


With関数を定義してみる

メンバーの数が多くなってくると、あるメンバーだけを変更したオブジェクトの生成が面倒になります。

例えばF#だと下記のようにwith構文が存在します。

{ p with Age = 20 }

C++で特殊構文は作成できないので、メンバーそれぞれのWith関数を定義してみます。

struct Person {
    const std::string Name;
    const int Age;
    Person(std::string name, int age) : Name(name), Age(age) {}
    Person WithName(std::string x) { return Person(x, Age); }
    Person WithAge(int x) { return Person(Name, x); }
};

Person p("Pieter", 10);
Person p2 = p.WithAge(20);


右辺値用のWith関数

現状、メンバー数が多くWith関数が続いてしまうような場合、途中でオブジェクトがたくさん生成されて効率があまりよくありません。

そこで右辺値の場合は自身のメンバを変更して返すようにしてみます。(残念ですがconstを外す以外の方法が思いつきませんでした。)

Person&& WithAge(int x) && {
    *(int*)&Age = x;
    return std::move(*this);
}

// 左辺値用
Person WithAge(int x) const & { return Person(Name, x); }

これにより、下記の記述でも最初のコンストラクトと最後のムーブコンストラクトの二回しかオブジェクトを生成しません。

auto p = Person("Pieter", 10).WithAge(20).WithAge(30).WithAge(40);


実際に利用する場合

C++では変数をconstにする機能があるため、メンバーまでconstにする必要はあまりないと思われます。

ですので下記で十分だと思います。

struct Person {
    std::string Name;
    int Age;
    Person(std::string name, int age) : Name(name), Age(age) {}

    Person WithAge(int x) const & {
        auto copied = *this;
        copied.Age = x;
        return copied;
    }
    Person&& WithAge(int x) && {
        Age = x;
        return std::move(*this);
    }

    // WithNameは省略
};

With関係はマクロにするとこんな感じになります。(変数のセットもmoveにしておきました。)

#define WITH(name)                                         \
    decltype(auto) With##name(decltype(name) x) const & {  \
        auto copied = *this;                               \
        copied.name = std::move(x);                        \
        return copied;                                     \
    }                                                      \
    decltype(auto) With##name(decltype(name) x) && {       \
        name = std::move(x);                               \
        return std::move(*this);                           \
    }

struct Person {
    std::string Name;
    int Age;
    Person(std::string name, int age) : Name(name), Age(age) {}
    WITH(Name)
    WITH(Age)   
};

コンストラクタもマクロにしたい場合は下記を参照してみてください。
Boost Preprocessorでコンストラクタ生成 - 何でもプログラミング