如何编写 C++ getter 和 setter

How to write C++ getters and setters

提问人:bolov 提问时间:7/31/2018 最后编辑:bolov 更新时间:8/19/2021 访问量:133540

问:

如果我需要编写一个 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_elementset_element

C getter-setter c++-faq

评论

9赞 Cheers and hth. - Alf 7/31/2018
为什么不直接让它成为公共数据成员
4赞 bolov 7/31/2018
@Cheersandhth.-Alf 最后一段:“假设对 getter 和 setter 的需求是合理的,例如,也许我们在 setter 中做一些检查,或者我们只写 getter)。问题是一旦你确定你需要写一个,如何写一个。
4赞 Caleth 7/31/2018
通常不需要包含单词 或 。它们都可以命名为 x()get_set_
2赞 Passer By 7/31/2018
我不确定问题出在哪里。看完答案,我不知道他们解决了什么问题。
2赞 bolov 7/31/2018
@2785528 getter 和 setter,并不总是合理的。我不对此提出异议。他们有时不被理解和滥用。话虽如此,有时它们是有道理的。这篇文章不是关于何时以及为什么使用它们。

答:

36赞 bolov 7/31/2018 #1

这就是我编写通用 setter/getter 的方式:

class Foo
{
private:
    X x_;

public:
    X&       x()        { return x_; }
    const X& x() const  { return x_; }
};

我将尝试解释每个转换背后的原因:

您的版本的第一个问题是,您应该传递 const 引用,而不是传递值。这避免了不必要的复制。没错,因为值可以移动,但这并不总是可行的。对于基本数据类型(例如),使用值而不是引用是可以的。C++11int

因此,我们首先对此进行纠正。

class Foo1
{
private:
    X x_;

public:
    void set_x(const X& value)
//             ^~~~~  ^
    {
        x_ = value;
    }

    const X& get_x()
//  ^~~~~  ^
    {
        return x_;
    }
};

上述解决方案仍然存在问题。由于不修改对象,因此应将其标记为 。这是称为常量正确性的 C++ 原则的一部分。get_xconst

上面的解决方案不允许你从对象中获取属性: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 对改进这篇文章的宝贵建议。

评论

2赞 NathanOliver 7/31/2018
为什么你的吸气剂要提供参考?这样做会破坏封装。即使传递 a 也是一个问题,因为我可以抛弃恒常性,现在我可以直接访问该成员。const&
2赞 bolov 7/31/2018
@NathanOliver,如果你抛弃了恒常性,你就是在做一些你不应该做的事情。
1赞 NathanOliver 7/31/2018
@bolov 为什么?将恒常性从非恒定性的东西中剔除是完全合法的。
1赞 StoryTeller - Unslander Monica 7/31/2018
@bolov - 我确实理解这一点。这就是为什么我专注于我发现令人困惑的事情。我认为自己相当精通 C++,新手会如何处理它?
1赞 bolov 7/31/2018
@NathanOliver合法的(在特定情况下)是的。但你不应该这样做。如果我给你一个恒定的参考,那是有原因的。你不应该放弃引用(尽管它可能是合法的)。另外,您不知道数据成员是否为 const,这将使它成为 UB。
0赞 gabry 7/31/2018 #2

您的主要错误是,如果您不在 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)。

69赞 Caleth 7/31/2018 #3

标准库中出现了两种截然不同的“属性”形式,我将其归类为“面向身份”和“面向价值”。选择哪种方式取决于系统应如何与 进行交互。两者都不是“更正确”。Foo

面向身份

class Foo
{
     X x_;
public:
          X & x()       { return x_; }
    const X & x() const { return x_; }
}

在这里,我们返回对基础成员的引用,它允许调用站点的两端观察另一方发起的更改。该成员对外界可见,大概是因为它的身份很重要。乍一看,它可能看起来只有属性的“获取”端,但如果是可分配的,则情况并非如此。XXX

 Foo f;
 f.x() = X { ... };

以价值为导向

class Foo
{
     X x_;
public:
     X x() const { return x_; }
     void x(X x) { x_ = std::move(x); }
}

在这里,我们返回成员的副本,并接受要覆盖副本。两端的后续更改都不会传播。想必在这种情况下,我们只关心 的值。Xx

评论

2赞 Karl Nicoll 7/31/2018
您能否举例说明每种类型的属性在标准库中的使用位置?
2赞 Caleth 7/31/2018
@KarlNicoll我在软件工程上得到了一个相关的答案,里面有例子
0赞 463035818_is_not_an_ai 8/21/2020
我认为“身份”和“面向价值”之间的区别在于数据是否真的被封装了。例如,只有在第二个中,才能计算修改的频率,而在第一个中,“getters”只能执行调用者可选的事情,例如与直接访问Fooxat
0赞 Caleth 8/21/2020
@idclev463035818 有时你不想封装。 如果你不能引用这些元素,那将是非常令人讨厌的。std::vector
1赞 luator 12/19/2022
在“面向身份”的情况下,这种实现与简单地公开相比有什么优势吗?x_
38赞 Jerry Coffin 7/31/2018 #4

多年来,我开始相信 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 之间。有两种方法可以处理这样的事情。一种是使用与上面相同的模板,但基础类型是 ,然后从那里开始。如果你的关系真的很复杂,你最终可能想要完全定义一个单独的类来定义该复杂关系中的对象。foobarfoostd::tuple<int, int>

总结

将你的成员定义为你真正想要的类型,并且 getter/setter 可以/将要做的所有有用的事情都包含在该类型的属性中。

评论

1赞 Lou 2/8/2021
一个新手问题:为什么构造函数不接受,而是使用 ?checkerstd::function<T(T const &)>template <class checker>
0赞 Bob 6/6/2022
也许,对于简单的东西,getters 和 setter 是一个错误——在有任何需求之前,增加了比需要的更多的复杂性,所以你只是开辟了一个新的错误源,而没有任何直接的好处。但是考虑多线程代码,您需要保护变量免受“多次更新”问题的影响,或者将它们包装在关键部分中。在这一点上,getter 和 setter 成为简单的途径,不需要苛刻的特定使用模式(例如,“你必须先锁定它,然后才能调用它”类型的规则)。尽早这样做是一个错误,但它也有良好和有效的用途。
0赞 Jerry Coffin 6/6/2022
@Bob 在我看来,对于多线程来说,就像我上面用类展示的那样,是一条简单的路线——既易于编写,又(与显式的 getter/setter 相反)易于使用且干净。checked
0赞 Sz. 5/4/2023
同意大多数 getter/setter 的冗余,但是“如果你的关系真的很复杂,你最终可能想要完全定义一个单独的类来定义该复杂关系中的对象”对我来说感觉像是真正的起点:很多时候我们已经在那个类中,并且试图解决这种复杂性(相互依赖)不仅仅是 getter/setter 只是简单的事情, 而不是将“复杂性的处理”推迟到另一层抽象/复杂性中?
0赞 Jerry Coffin 5/4/2023
@Sz.:没有看到一个具体的例子,很难说。我想它可能会证明 / 是合理的,但如果是这样,我还没有看到它......settergetter
-7赞 Hot Since 7cc 9/5/2018 #5

使用一些 IDE 进行生成。CLion 提供了基于类成员插入 getter 和 setter 的选项。从那里您可以看到生成的结果并遵循相同的做法。