如果我需要为属性编写一个setter和/或getter,则可以这样编写:
struct X { /*...*/};
class Foo
{
private:
X x_;
public:
void set_x(X value)
{
x_ = value;
}
X get_x()
{
return x_;
}
};
但是我听说这是编写设置方法和获取方法的 Java风格,我应该以C ++风格编写。而且我被告知这是不充分的,甚至是不正确的。这意味着什么?如何用C ++编写设置器和获取器?
假设需要使用吸气剂和/或塞特剂。例如。也许我们在设置器中进行了一些检查,或者也许我们只编写了获取器。
答案 0 :(得分:9)
在标准库中出现了两种不同形式的“属性”,我将其分为“面向身份”和“面向价值”。您选择哪种方式取决于系统应如何与Foo
进行交互。 “更正确”都不是。
以身份为导向
class Foo
{
X x_;
public:
X & x() { return x_; }
const X & x() const { return x_; }
}
在这里,我们向基础X
成员返回 reference ,这使呼叫站点的双方都能观察到对方发起的更改。 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
的 value 。
答案 1 :(得分:6)
这些年来,我开始相信,整个getter / setter概念基本上都是错误的。正如@Alf在评论中所建议的,听起来似乎相反,通常,公共变量是正确的答案。
诀窍在于,公共变量应为正确的类型。在问题中,您指定了要么我们写了一个setter来对所写的值进行一些检查,要么我们只写了一个getter(所以我们有一个有效的const
对象)。 / p>
我要说的是,这两个词基本上都在说:“ 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); });
然后我们可以使用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作为参数的赋值运算符。
当然,某些情况下输入受限会更加复杂。在某些情况下,您想要类似两件事之间的关系,因此foo
必须在0..1000的范围内,而bar
必须在2x到3x {{1}之间}。有两种方法可以处理这样的事情。一种是使用与上述相同的模板,但其基础类型为foo
,然后从那里开始。如果您的关系确实很复杂,那么您可能最终想要完全定义一个单独的类来定义该复杂关系中的对象。
将您的成员定义为您真正想要的类型,并且getter / setter可以/将要使用的所有有用的东西都归入该类型的属性中。
答案 2 :(得分:4)
这就是我编写通用的setter / getter的方式:
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
。这是C ++原理的一部分,称为 const正确性。
上述解决方案不允许您从const
对象获取属性:
const Foo1 f;
X x = f.get_x(); // Compiler error, but it should be possible
这是因为不能在const对象上调用不是const方法的get_x
。这样做的合理性是非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 ++风格而不是Java风格。
有两点要考虑:
因此,基于以上知识,我们可以编写最终的优雅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对改进此帖子的出色建议。
答案 3 :(得分:0)
您的主要错误是,如果您未在API参数和返回值中使用引用,那么您可能可能冒着在两个获取/设置操作(“ 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_
我发现在_或驼峰大小写(例如getX(),setX())中都没有使用get / set,如果您做错了什么,编译器将帮助您解决问题。
如果要修改内部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)。
答案 4 :(得分:0)
使用一些IDE进行生成。 CLion提供了基于类成员插入getter和setter的选项。从那里您可以看到生成的结果并遵循相同的做法。