【问题标题】:How to write C++ getters and setters如何编写 C++ getter 和 setter
【发布时间】:2019-01-07 23:12:39
【问题描述】:

如果我需要编写一个 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 是正确的解决方案。乍一看,它们可能并不像 setter 或 getter,但它们确实如此,或者至少适用于编写它们的模式。

例如:

  • 获取向量的大小。您不想公开数据成员,因为它需要是只读的。

  • Getter 和 setter 不需要只公开一个数据成员。考虑获取和设置数组的元素。那里有逻辑,您不能只公开一个数据成员,首先没有要公开的数据成员。它仍然是你无法避免的 getter/setter 对:

    class Vector
    {
        void set_element(std::size_t index, int new_value);
        int get_element(std::size_t index);
    };
    

    了解编写 getter 和 setter 的 C++ 惯用方式将使我能够以 C++ 惯用方式编写上述 get_element/set_element

【问题讨论】:

  • 为什么不把它设为公共数据成员
  • @Cheersandhth.-Alf 最后一段:“假设对 getter 和 setter 的需求是合理的,例如,也许我们在 setter 中做一些检查,或者我们只写 getter)。”问题是,一旦确定需要写一篇,如何写一篇。
  • 您通常不需要包含单词get_set_。他们可以both be named x()
  • 我不确定问题出在哪里。看完答案,我不知道他们解决了什么问题。
  • @2785528 getter 和 setter 并不总是合理的。我不反对。他们有时不被理解和滥用。话虽如此,有时他们是有道理的。这篇文章不是关于何时以及为什么使用它们。

标签: c++ getter-setter c++-faq


【解决方案1】:

这就是我编写通用 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_;
    }
};

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

上述解决方案不会让您从const 对象中获取属性:

const Foo1 f;

X x = f.get_x(); // Compiler error, but it should be possible

这是因为 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++ 而不是 Java。

有两点需要考虑:

  • 我们可以返回对数据成员的引用,如果我们修改该引用,我们实际上会修改数据成员本身。我们可以使用它来编写我们的 setter。
  • C++ 中的方法可以仅通过常量来重载。

所以有了以上知识,我们就可以编写出最终优雅的 C++ 版本了:

最终版本

class Foo
{
private:
    X x_;

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

作为个人喜好,我使用新的尾随返回函数样式。 (例如,我写的是auto foo() -> int,而不是int foo()

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_); }

};

非常感谢 cmets 收到的反馈,特别感谢 StorryTeller 对改进这篇文章的建议。

【讨论】:

  • 为什么你的 getter 提供参考?你这样做是在打破封装。即使传递 const& 也是一个问题,因为我可以将 constness 扔掉,现在我可以直接访问该成员。
  • @NathanOliver 如果你抛弃了你所做的事情,那么你就是在做你不应该做的事情。
  • @bolov 为什么?从非 const 的东西中抛弃 const 是完全合法的。
  • @bolov - 我明白这一点。这就是为什么我专注于我发现令人困惑的。我认为自己对 C++ 相当精通,新手如何处理?
  • @NathanOliver 法律(在指定情况下)是的。但你不应该这样做。如果我给你一个 const 参考,那是有原因的。你不应该抛弃引用(尽管它可能是合法的)。另外,您不知道数据成员是否为 const 会使其成为 UB。
【解决方案2】:

多年来,我开始相信 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); });

然后我们可以使用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;

有了这个,我们可以安全地将成员公开,因为我们定义它的类型确实是我们希望它成为的类型——我们想要放置在它上面的条件是类型固有的,而不是getter/setter 事后(可以这么说)附加的东西。

当然,这适用于我们希望以某种方式限制值的情况。如果我们只想要一个有效的只读类型,那就容易多了——只需要一个定义构造函数和 operator T 的模板,而不是一个以 T 作为参数的赋值运算符。

当然,某些输入受限的情况可能更复杂。在某些情况下,您想要两个事物之间的关系,因此(例如)foo 必须在 0..1000 范围内,bar 必须在 2x 和 3x foo 之间。有两种方法可以处理这样的事情。一种是使用与上面相同的模板,但基础类型是std::tuple&lt;int, int&gt;,然后从那里开始。如果您的关系真的很复杂,您可能最终想要完全定义一个单独的类来定义该复杂关系中的对象。

总结

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

【讨论】:

  • 一个新手问题:为什么checker的构造函数不接受std::function&lt;T(T const &amp;)&gt;,而是使用template &lt;class checker&gt;
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    身份导向

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

    这里我们返回一个 reference 到底层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

    【讨论】:

    • 您能否提供一个示例,说明每种属性在标准库中的使用位置?
    • @KarlNicoll 我有一个相关的答案at Software Engineering 有例子
    • 我想“身份”和“价值导向”之间的区别在于数据是否实际被封装。仅在第二个Foo 中,例如计算x 被修改的频率,而在第一个中,“getters”只能执行调用者可选的操作,例如at 与直接访问
    • @idclev463035818 有时您不想封装。如果你不能引用这些元素,std::vector 会非常令人讨厌。
    【解决方案5】:

    您的主要错误是,如果您没有在 API 参数和返回值中使用引用,那么您可能冒着在两个 get/set 操作中执行不需要的副本的风险(“可能”,因为如果您使用优化器,您的编译可能会避免这些副本)。

    我会这样写:

    class Foo
    {
    private:
        X x_;
    public:
        void x(const X &value) { x_ = value; }
        const X &x() const { return x_; }
    };
    

    这将保持 const 正确性,这是 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)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-27
      • 1970-01-01
      • 2016-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多