如何编写 C++ getter 和 setter

How to write C++ getters and setters

如果我需要写一个setter and/or getter 我这样写:

struct X { /*...*/};

class Foo
{
private:
    X x_;

public:
    void set_x(X value)
    {
        x_ = value;
    }
    X get_x()
    {
        return x_;
    }
};

但是我听说这是 Java 风格 写 setters 和 getters 并且我应该写它以 C++ 风格。此外,有人告诉我这是低效的,甚至是不正确的。那是什么意思?如何在 C++ 中编写 setters 和 getters?


假设需要 getters and/or setters 是合理的。例如。也许我们在 setter 中做一些检查,或者我们只写 getter.

关于不需要 getters 和 setters 的讨论很多。虽然我同意这里所说的大部分内容,但我仍然主张需要知道如何以惯用的方式编写此类方法,因为有正当理由认为 getters 和 setters 是正确的解决方案。乍一看,它们可能不像 setter 或 getter,但它们确实是,或者至少适用于编写它们的模式。

例如:

这就是我编写通用程序的方式 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。这是称为 const correctness.

的 C++ 原则的一部分

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

const Foo1 f;

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

这是因为 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 ish。

有两点需要考虑:

  • 我们可以 return 对数据成员的引用,如果我们修改该引用,我们实际上修改了数据成员本身。我们可以用它来写我们的 setter.
  • 在 C++ 中,方法可以仅通过 consteness 重载。

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

最终版本

class Foo
{
private:
    X x_;

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

作为个人喜好,我使用新的尾随 return 函数样式。 (例如,我写 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();

超越最终版本

出于性能原因,我们可以更进一步,在 && 和 return 上重载对 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_); }

};

非常感谢您在评论中收到的反馈,特别是感谢 StoryTeller 提出的改进此 post 的重要建议。

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

我会写成:

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

这将保持 const 正确性,这是 C++ 的一个非常重要的特性,并且它与旧的 C++ 版本兼容(另一个答案需要 c++11)。

您可以将此 class 用于:

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)。

标准库中出现了两种不同形式的 "properties",我将其归类为 "Identity oriented" 和 "Value oriented"。您选择哪个取决于系统应如何与 Foo 交互。 "more correct".

也不是

身份导向

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

这里我们 return 对基础 X 成员的 引用 ,这允许调用站点的双方观察对方发起的更改。 X 成员对外界可见,大概是因为它的身份很重要。乍一看似乎只有 属性 的 "get" 一侧,但如果 X 是可分配的,则情况并非如此。

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

价值导向

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

这里我们return一个X成员的copy,并接受一个copy来覆盖.任何一方后来的变化都不会传播。大概在这种情况下我们只关心x

多年来,我逐渐相信 getter/setter 的整个概念通常是错误的。与听起来相反,public 变量通常是正确答案。

诀窍是 public 变量应该是正确的类型。在问题中,您已经指定我们已经编写了一个 setter 来对正在写入的值进行一些检查,或者我们只编写了一个 getter (所以我们有一个有效的 const对象)。

我想说的是,这两个基本上都在说:“X 是一个整数。只是它不是真正的整数——它实际上有点像整数,但有这些额外的限制……”

这让我们进入了真正的重点:如果仔细查看 X 表明它确实是一个不同的类型,那么定义它真正的类型,然后将其创建为 public 的成员那种。它的骨架可能看起来像这样:

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)来确保值是正确的——它可以不加改变地传递值,也可以修改它(例如,对于饱和类型)或者它可能抛出异常——但如果它不抛出,它 returns 必须是指定类型可接受的值。

因此,例如,要获得一个整数类型,它只允许 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;

有了这个,我们就可以安全地创建成员 public,因为我们定义它的类型确实是我们想要它成为的类型——我们想要放在它上面的条件是类型固有的,而不是 getter/setter.

在事后(可以这么说)附加的东西

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

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

总结

将您的成员定义为您真正想要的类型,并且 getter/setter could/would 确实包含在该类型的属性中。

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