提问人:bolov 提问时间:7/31/2018 最后编辑:bolov 更新时间:8/19/2021 访问量:133540
如何编写 C++ getter 和 setter
How to write C++ getters and setters
问:
如果我需要编写一个 setter 和/或 getter,我是这样写的:
struct X { /*...*/};
class Foo
{
private:
X x_;
public:
void set_x(X value)
{
x_ = value;
}
X get_x()
{
return x_;
}
};
但是,我听说这是编写 setter 和 getter 的 Java 风格,我应该用 C++ 风格编写它。此外,我被告知这是不熟练的,甚至是不正确的。那是什么意思?如何在 C++ 中编写 setter 和 getter?
假设对 getter 和/或 setter 的需求是合理的。例如,也许我们在 setter 中做一些检查,或者我们只编写 getter。
有很多关于不需要 getter 和 setter 的喋喋不休。虽然我同意这里所说的大部分内容,但我仍然主张需要知道如何用惯用语编写这样的方法,因为有正当的理由表明 getter 和 setter 是正确的解决方案。乍一看,它们可能不是二传手或吸食者,但他们确实是,或者至少编写它们的模式适用。
例如:
获取向量的大小。您不希望公开数据成员,因为它需要是只读的。
getter 和 setter 不需要只公开数据成员。考虑获取和设置数组的元素。这是有逻辑的,你不能只公开一个数据成员,首先没有要公开的数据成员。它仍然是你无法避免的 getter/setter 对:
class Vector { void set_element(std::size_t index, int new_value); int get_element(std::size_t index); };
了解 C++ 编写 getter 和 setter 的惯用方式将允许我以 C++ 惯用方式编写上述 /。
get_element
set_element
答:
这就是我编写通用 setter/getter 的方式:
class Foo
{
private:
X x_;
public:
X& x() { return x_; }
const X& x() const { 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_;
}
};
上述解决方案仍然存在问题。由于不修改对象,因此应将其标记为 。这是称为常量正确性的 C++ 原则的一部分。get_x
const
上面的解决方案不允许你从对象中获取属性:const
const Foo1 f;
X x = f.get_x(); // Compiler error, but it should be possible
这是因为不能在 const 对象上调用不是 const 方法。这样做的理由是,非 const 方法可以修改对象,因此在 const 对象上调用它是非法的。get_x
因此,我们进行了必要的调整:
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。
有两件事需要考虑:
- 我们可以返回对数据成员的引用,如果我们修改该引用,我们实际上会修改数据成员本身。我们可以用它来编写我们的 setter。
- 在 C++ 中,方法可以仅通过连续性重载。
因此,有了上述知识,我们就可以编写最终的优雅 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 对改进这篇文章的宝贵建议。
评论
const&
您的主要错误是,如果您不在 API 参数和返回值中使用引用,那么您可能会冒着在 get/set 操作中执行不需要的副本的风险(“MAY”,因为如果您使用优化器,您的编译可能会避免这些副本)。
我会写成:
class Foo
{
private:
X x_;
public:
void x(const X &value) { x_ = value; }
const X &x() const { return x_; }
};
这将保持常量正确性,这是 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() 的第三个重载:
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)。
标准库中出现了两种截然不同的“属性”形式,我将其归类为“面向身份”和“面向价值”。选择哪种方式取决于系统应如何与 进行交互。两者都不是“更正确”。Foo
面向身份
class Foo
{
X x_;
public:
X & x() { return x_; }
const X & x() const { return x_; }
}
在这里,我们返回对基础成员的引用,它允许调用站点的两端观察另一方发起的更改。该成员对外界可见,大概是因为它的身份很重要。乍一看,它可能看起来只有属性的“获取”端,但如果是可分配的,则情况并非如此。X
X
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
x
评论
Foo
x
at
std::vector
x_
多年来,我开始相信 getter/setter 的整个概念通常是一个错误。尽管听起来恰恰相反,但公共变量通常是正确的答案。
诀窍是公共变量的类型应该正确。在问题中,您指定了我们编写了一个 setter 来检查正在写入的值,或者我们只编写一个 getter(因此我们有一个有效的对象)。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;
}
};
这是通用的,因此用户可以指定类似函数的东西(例如,lambda)来确保值是正确的 - 它可能会原封不动地传递值,或者它可能会修改它(例如,对于饱和类型)或者它可能会抛出异常 - 但如果它不抛出,它返回的内容必须是指定类型可接受的值。
因此,例如,要获得一个只允许从 0 到 10 的值,并在 0 和 10 处达到饱和的整数类型(即,任何负数变为 0,任何大于 10 的数字变为 10),我们可以按照以下一般顺序编写代码:
checked<int> foo([](auto i) { return std::min(std::max(i, 0), 10); });
然后我们可以用 或多或少地做通常的事情,并保证它始终在 0..10 范围内:foo
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;
有了这个,我们可以安全地将成员公开,因为我们定义的类型实际上是我们想要的类型——我们想要对它施加的条件是类型固有的,而不是事后(可以这么说)由 getter/setter 附加的东西。
当然,这是针对我们想要以某种方式限制值的情况。如果我们只想要一个有效的只读类型,那就容易多了——只是一个定义构造函数和 的模板,而不是一个将 T 作为其参数的赋值运算符。operator T
当然,某些受限输入的情况可能更复杂。在某些情况下,您想要两个事物之间的关系,因此(例如)必须在 0..1000 范围内,并且必须介于 2x 和 3x 之间。有两种方法可以处理这样的事情。一种是使用与上面相同的模板,但基础类型是 ,然后从那里开始。如果你的关系真的很复杂,你最终可能想要完全定义一个单独的类来定义该复杂关系中的对象。foo
bar
foo
std::tuple<int, int>
总结
将你的成员定义为你真正想要的类型,并且 getter/setter 可以/将要做的所有有用的事情都包含在该类型的属性中。
评论
checker
std::function<T(T const &)>
template <class checker>
checked
setter
getter
使用一些 IDE 进行生成。CLion 提供了基于类成员插入 getter 和 setter 的选项。从那里您可以看到生成的结果并遵循相同的做法。
评论
x()
get_
set_