繁体   English   中英

如何编写 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 是正确的解决方案是有正当理由的。 乍一看,他们可能不会将它们视为 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

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

身份导向

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

这里我们返回一个底层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); }
}

在这里,我们返回的副本X成员,并接受一个副本覆盖。 任何一方的后期更改都不会传播。 在这种情况下,大概我们只关心x

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

诀窍是公共变量应该是正确的类型。 在您指定的问题中,要么我们编写了一个 setter 来对正在写入的值进行一些检查,要么我们只编写一个 getter(因此我们有一个有效的const对象)。

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

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

概括

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

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

class Foo
{
private:
    X x_;

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

我将尝试解释每个转换背后的推理:

您的版本的第一个问题是,您应该传递常量引用而不是传递值。 这避免了不必要的复制。 是的,因为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++ 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 对改进这篇文章提出的很好的建议。

您的主要错误是,如果您不在 API 参数和返回值中使用引用,那么您可能会冒着在 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)。

您可以将此类用于:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM