C++ における const または参照メンバー変数の意味

C++ コミュニティの常識では、非静的 09 または参照データ変数は問題があると見なされます.驚くべきことに、このトピック専用のリソースが 1 つも見つかりません.

この記事を書くことにしたのは、Twitter や #include <C++> Discord サーバーで同じ問題が複数回発生したためです。 15 を避けるべき確かな理由があります。 または C++ ではメンバー変数を参照します。ただし、C++ の多くのことと同様に、「避ける」は「決して使用しない」という意味ではありません。

定数メンバー

29 を扱う Rust などのプログラミング言語に精通している場合 デフォルトであり、二流市民として変更可能であるため、すべてを 31 とマークしたくなるかもしれません 無数の Jason Turner と Kate Gregory の講演が示すように、このプラクティスは C++ でも多くのメリットをもたらします。 除くすべて メンバー変数。"

51 メンバー変数は割り当てを無効にします および移動セマンティクス 代入の場合、どうすれば定数に何かを代入できるので、理にかなっていますか?移動セマンティクスの場合、技術的にはコピーは有効な移動の実装ですが、型システムは移動後の状態が同じままであることを保証できません。

「大したことは何ですか? 私はすでにフィールドを変異させたくないと言いました。」

64 を除いて 割り当ての両方を使用 および移動セマンティクス .move 操作がなければ、すべての move はコピーにフォールバックします。 コンパイルに失敗しました:

struct BadImmutablePoint {
    const int x = 0;
    const int y = 0;
};

int main() {
  BadImmutablePoint p1;
  BadImmutablePoint p2 {42, 55};
  std::swap(p1, p2); // Error
}

これは、代入を内部で使用するすべての STL 機能に対してもノーを意味します。たとえば、82 :

std::vector<BadImmutablePoint> points;
// Sort by x-axis
std::ranges::sort(points, {}, &BadImmutablePoint::x); // Error

しかし、メンバー変数を変更したくありません!

C++ でできる最善のことは、メンバー変数を 97 にすることです。 getter.Access コントロールのみを公開しても、クラスの内部がメンバーを変更するのを防ぐことはできませんが、少なくとも現在は、クラスの外部のすべてができません。

class ImmutablePoint {
    int x_ = 0;
    int y_ = 0;

public:
    constexpr ImmutablePoint() = default;
    constexpr ImmutablePoint(int x, int y) : x_{x}, y_{y} {}
    [[nodiscard]] constexpr auto x() const -> int { return x_; }
    [[nodiscard]] constexpr auto y() const -> int { return y_; }
};

int main() {
    std::vector<ImmutablePoint> points;
    ...
    std::ranges::sort(points, {}, &ImmutablePoint::x); // Ok
}

この getter による並べ替えの行は、上記のメンバー変数による並べ替えとまったく同じであることに注意してください。C++20 の範囲射影は優れた機能です。

これは定型句のようなものです。正直に言うと、この特定のケースでは、非定数フィールドを含む集計に固執します。ポイント変数を作成するときは、ポイント全体を 105 :

struct Point {
    int x = 0;
    int y = 0;
};

const Point immutable_point {42, 55};

本当を取得したい場合 ちょっとしたテンプレートを作成して、getter のみを公開するプロセスを自動化することもできます.ただし、私自身はここまでは行きません.

template <typename T>
class const_wrapper {
    T val_;
public:
    constexpr const_wrapper(const T& val) : val_{val} {}
    constexpr const_wrapper(T&& val) : val_{std::move(val)} {}

    [[nodiscard]] constexpr auto get() const -> const T& { return val_; }
    [[nodiscard]] constexpr operator T() const { return val_; }
};

これを 114 にする たとえば、型制約付きの可変個引数テンプレート コンストラクターを追加することにより、より便利なクラス テンプレートを作成することは、このクラスを本当に使用したい読者のために残された課題です 😉.

次に、このテンプレートを次のように使用できます:

struct ImmutablePoint {
    const_wrapper<int> x = 0;
    const_wrapper<int> y = 0;
};

int main() {
    std::vector<ImmutablePoint> points;
    ...
    std::ranges::sort(points, {}, &ImmutablePoint::x); // Ok
}

メンバー変数を参照する

Java や Python などの他の多くのプログラミング言語のポインターまたは「参照」とは異なり、C++ 参照は再バインドできません。したがって、123 と非常によく似た状況になります。 members. 参照の良い例えは 136 です null にできないポインター。たとえば、以下の 146 150 と同じ問題を抱えています 165 で フィールド。

struct BadImmutableTriangle {
    const ImmutablePoint& a;
    const ImmutablePoint& b;
    const ImmutablePoint& c;
};

const データ メンバーのソリューションと同様に、参照データ メンバーを格納する代わりに、ポインター メンバーを格納し、参照ゲッターのみを公開できます。

class ImmutableTriangle {
    const ImmutablePoint* a_;
    const ImmutablePoint* b_;
    const ImmutablePoint* c_;

public:
    // No default constructor
    constexpr ImmutableTriangle(
        const ImmutablePoint& a,
        const ImmutablePoint& b,
        const ImmutablePoint& c)
        : a_{&a}, b_{&b}, c_{&c} {}

    [[nodiscard]] constexpr auto a() const -> const ImmutablePoint& { return *a_; }
    [[nodiscard]] constexpr auto b() const -> const ImmutablePoint& { return *b_; }
    [[nodiscard]] constexpr auto c() const -> const ImmutablePoint& { return *c_; }
};

C++ 標準ライブラリには 179 が含まれています ヘルパー テンプレートであり、180 のように機能します。

struct ImmutableTriangle {
    std::reference_wrapper<const ImmutablePoint> a;
    std::reference_wrapper<const ImmutablePoint> b;
    std::reference_wrapper<const ImmutablePoint> c;
};

190 202 より便利です .参照セマンティクスを維持しながらコンテナに何かを格納しようとする場合に不可欠であるため:

std::vector<ImmutablePoint&> triangles1; // Error
std::vector<std::reference_wrapper<ImmutablePoint>> triangles2; // Ok
std::vector<ImmutablePoint*> triangles3; // Ok, with caveat

コード 212 225 をソートします また、関連する比較演算子を定義していない場合、コンパイルに失敗します。三角形の明確なデフォルトの順序付けがないため、これは望ましい動作です。一方、239 コンパイルされますが、ポインターのアドレスでソートされます。この種の非決定的な動作は望ましくありません。

どこで 240 または参照メンバー変数は引き続き使用できます

場合によっては、割り当て操作と移動操作を既に無効にしているか、とにかく独自の操作を記述する必要があります。主な例の 1 つは継承階層です。そのような場合は、252 を使用しても問題ありません。 またはメンバー変数を参照します。

263 の別の使用例 または参照メンバーはローカル関数オブジェクトにあり、代入の動作は気にしません。たとえば、ラムダ式で参照によってキャプチャされた変数は、参照メンバー変数に脱糖されます。

結論

C++ の中核は、C の遺産に基づいて構築された命令型言語であり、272 また、コア言語メカニズムは代入に大きく依存しています。好むと好まざるとにかかわらず、C++ クラスを作成するときに、メンバー変数を変更するユーザーの自由を制限することは十分にサポートされていません。