プロパティのセッターやゲッターを記述する必要がある場合は、次のように記述します。
struct X { /*...*/};
class Foo
{
private:
X x_;
public:
void set_x(X value)
{
x_ = value;
}
X get_x()
{
return x_;
}
};
しかし、これはセッターとゲッターを書くJavaスタイルであり、C++スタイルで書くべきだと聞いたことがあります。さらに、それは非能率的で、さらには不正確であると言われました。どういう意味ですか? C++でセッターとゲッターを作成するにはどうすればよいですか?
ゲッターやセッターの必要性が正当化されていると想定。例えば。多分私たちはセッターでいくつかのチェックをするか、あるいはゲッターだけを書くでしょう。
標準ライブラリでは、「プロパティ」には2つの異なる形式があり、「アイデンティティ指向」と「値指向」に分類します。どちらを選択するかは、システムがFoo
と対話する方法によって異なります。どちらも「より正確」ではありません。
アイデンティティ指向
class Foo
{
X x_;
public:
X & x() { return x_; }
const X & x() const { return x_; }
}
ここでは、基になるX
メンバーにreferenceを返します。これにより、呼び出しサイトの両側で、他方が開始した変更を監視できます。 X
メンバーは、IDが重要であるためと考えられ、外部に表示されます。一見、プロパティの「取得」側しかないように見えるかもしれませんが、X
が割り当て可能な場合はそうではありません。
Foo f;
f.x() = X { ... };
価値志向
class Foo
{
X x_;
public:
X x() const { return x_; }
void x(X x) { x_ = std::move(x); }
}
ここでは、X
メンバーのcopyを返し、上書きするcopyを受け入れます。その後のどちらかの側の変更は伝播しません。おそらくこの場合、x
の-valueのみに注意します。
長年にわたって、ゲッター/セッターの概念全体は本質的に常に間違いであると信じるようになりました。 @Alfがコメントで示唆しているように、逆に聞こえるかもしれませんが、通常はパブリック変数が正しい答えです。
トリックは、パブリック変数が正しいタイプである必要があるということです。質問では、書き込まれる値のチェックを行うセッターを作成したか、またはゲッターのみを作成したことを指定しました(そのため、実質的にconst
オブジェクトがあります)。
どちらも基本的には「Xはintです。実際にはintではありません。実際にはintのようなものですが、これらの追加の制限があります...」
そして、それは本当のポイントに私たちを導きます:Xを注意深く見ると、それが本当に異なるタイプであることが示されたら、それが本当にあるタイプを定義し、次にそのタイプのパブリックメンバーとして作成します。基本的には次のようになります。
template <class T>
class checked {
T value;
std::function<T(T const &)> check;
public:
template <class checker>
checked(checker check)
: check(check)
, value(check(T()))
{ }
checked &operator=(T const &in) { value = check(in); return *this; }
operator T() const { return value; }
friend std::ostream &operator<<(std::ostream &os, checked const &c) {
return os << c.value;
}
friend std::istream &operator>>(std::istream &is, checked &c) {
try {
T input;
is >> input;
c = input;
}
catch (...) {
is.setstate(std::ios::failbit);
}
return is;
}
};
これは一般的なものであるため、ユーザーは、値が正しいことを保証する関数のようなもの(ラムダなど)を指定できます。値が変更されずに渡されるか、(たとえば、飽和型の場合)変更される場合があります。例外をスローする可能性がありますが、スローしない場合、返されるものは、指定されている型に受け入れられる値でなければなりません。
したがって、たとえば、0〜10の値のみを許可し、0と10で飽和する整数型を取得するには(つまり、負の数は0になり、10より大きい数は10になります)、この一般的なコードを記述します。注文:
checked<int> foo([](auto i) { return std::min(std::max(i, 0), 10); });
次に、foo
を使用して通常のことを行うことができ、常に0..10の範囲になることが保証されます。
std::cout << "Please enter a number from 0 to 10: ";
std::cin >> foo; // inputs will be clamped to range
std::cout << "You might have entered: " << foo << "\n";
foo = foo - 20; // result will be clamped to range
std::cout << "After subtracting 20: " << foo;
これを使用して、メンバーをパブリックに安全に作成できます。これは、メンバーとして定義したタイプが、実際に希望するタイプであるためです。メンバーに課したい条件は、タイプされたものではなく、タイプに固有のものです。 (いわば)ゲッター/セッターによる事実の後。
もちろん、これは何らかの方法で値を制限したい場合のためのものです。実質的に読み取り専用の型が必要な場合は、はるかに簡単です。コンストラクターとoperator T
を定義するテンプレートだけで、Tをパラメーターとして取る代入演算子は必要ありません。
もちろん、制限された入力のいくつかのケースはより複雑になる可能性があります。場合によっては、2つのものの間の関係などが必要になることがあります。たとえば、foo
は0..1000の範囲内で、bar
は2xから3x foo
の間でなければなりません。このようなことを処理するには2つの方法があります。 1つは上記と同じテンプレートを使用することですが、基になる型はstd::Tuple<int, int>
であり、そこから移動します。関係が本当に複雑な場合は、別のクラスを完全に定義して、その複雑な関係のオブジェクトを定義したいと思うかもしれません。
メンバーを実際に必要なタイプに定義し、ゲッター/セッターが実行できる/実行するすべての便利なことをそのタイプのプロパティに包括します。
これは私が一般的なセッター/ゲッターを書く方法です:
_class Foo
{
private:
X x_;
public:
auto x() -> X& { return x_; }
auto x() const -> const X& { return x_; }
};
_
各変換の背後にある理由を説明しようとします。
バージョンの最初の問題は、値を渡すのではなく、const参照を渡す必要があることです。これにより、不必要なコピーが回避されます。確かに、_C++11
_から値を移動できますが、常に移動できるとは限りません。基本的なデータ型(例:int
)では、参照の代わりに値を使用しても問題ありません。
したがって、最初にそれを修正します。
_class Foo1
{
private:
X x_;
public:
void set_x(const X& value)
// ^~~~~ ^
{
x_ = value;
}
const X& get_x()
// ^~~~~ ^
{
return x_;
}
};
_
それでも上記の解決策には問題があります。 _get_x
_はオブジェクトを変更しないため、const
とマークする必要があります。これは、const correctnessと呼ばれるC++原則の一部です。
上記のソリューションでは、const
オブジェクトからプロパティを取得できません。
_const Foo1 f;
X x = f.get_x(); // Compiler error, but it should be possible
_
これは、constメソッドではない_get_x
_をconstオブジェクトで呼び出すことができないためです。この理由は、非constメソッドがオブジェクトを変更できるため、constオブジェクトで呼び出すことは違法であるということです。
したがって、必要な調整を行います。
_class Foo2
{
private:
X x_;
public:
void set_x(const X& value)
{
x_ = value;
}
const X& get_x() const
// ^~~~~
{
return x_;
}
};
_
上記のバリアントは正しいです。ただし、C++では、C++ ishが多く、Java ish。
考慮すべき点が2つあります。
上記の知識があれば、最終的なエレガントなC++バージョンを作成できます。
_class Foo
{
private:
X x_;
public:
X& x() { return x_; }
const X& x() const { return x_; }
};
_
個人的な好みとして、新しい末尾の戻り関数スタイルを使用します。 (例:int foo()
の代わりにauto foo() -> int
と書きます。
_class Foo
{
private:
X x_;
public:
auto x() -> X& { return x_; }
auto x() const -> const X& { return x_; }
};
_
次に、呼び出し構文を次のように変更します。
_Foo2 f;
X x1;
f.set_x(x1);
X x2 = f.get_x();
_
に:
_Foo f;
X x1;
f.x() = x1;
X x2 = f.x();
_
_const Foo cf;
X x1;
//cf.x() = x1; // error as expected. We cannot modify a const object
X x2 = cf.x();
_
パフォーマンス上の理由から、さらに進んで_&&
_をオーバーロードし、_x_
_への右辺値参照を返すことができるため、必要に応じてそこから移動できます。
_class Foo
{
private:
X x_;
public:
auto x() const& -> const X& { return x_; }
auto x() & -> X& { return x_; }
auto x() && -> X&& { return std::move(x_); }
};
_
コメントで受け取ったフィードバック、特にStorryTellerがこの投稿を改善するための素晴らしい提案をしてくれたことに感謝します。
主なエラーは、APIパラメータと戻り値で参照を使用しない場合、may両方のget/set操作で不要なコピーを実行するリスクがある( "MAY"は、オプティマイザを使用する場合、あなたのコンパイルはおそらくこれらのコピーを避けることができるでしょう)。
私はそれを次のように書きます:
class Foo
{
private:
X x_;
public:
void x(const X &value) { x_ = value; }
const X &x() const { return x_; }
};
これはconst correctnessを維持します。これはC++の非常に重要な機能であり、古いC++バージョンと互換性があります(他の回答にはc ++ 11が必要です)。
このクラスは以下で使用できます。
Foo f;
X obj;
f.x(obj);
X objcopy = f.x(); // get a copy of f::x_
const X &objref = f.x(); // get a reference to f::x_
私はget/setの使用が_またはキャメルのケース(つまりgetX()、setX())の両方で不必要であることを発見しました。何か間違ったことをした場合、コンパイラはそれを整理するのに役立ちます。
内部のFoo :: Xオブジェクトを変更したい場合は、x()の3番目のオーバーロードを追加することもできます。
X &x() { return x_; }
..このようにして、次のように書くことができます。
Foo f;
X obj;
f.x() = obj; // replace inner object
f.x().int_member = 1; // replace a single value inside f::x_
しかし、内部構造体(X)を頻繁に変更する必要がある場合を除いて、これを回避することをお勧めします。
いくつかのIDEを生成に使用します。CLionは、クラスメンバーに基づいてゲッターとセッターを挿入するオプションを提供します。そこから、生成された結果を確認し、同じ方法に従うことができます。