【问题标题】:C++ getters/setters coding styleC++ getter/setter 编码风格
【发布时间】:2010-10-20 03:12:51
【问题描述】:

我用 C# 编程已经有一段时间了,现在我想复习一下我的 C++ 技能。

上课:

class Foo
{
    const std::string& name_;
    ...
};

最好的方法是什么(我只想允许读取 name_ 字段):

  • 使用getter方法:inline const std::string& name() const { return name_; }
  • 公开该字段,因为它是一个常量

谢谢。

【问题讨论】:

  • 顺便说一句,在 C++ 中为成员变量使用单个前导下划线更为常见。
  • 我还认为结尾的下划线有点别扭,但我看到的是在 C++ Faq Lite 中使用的。
  • 有关在 C++ 中使用下划线的完整答案,请参见以下内容stackoverflow.com/questions/228783/…
  • @MartinBeckett 单下划线可能很常见,但作为一个有视力障碍的人,我可以告诉你它们读起来很痛苦。 IDE 只是喜欢在内容(错误、拼写等)下划线,而下划线会被“帮助”完全掩盖。对于整个编码社区,请听我们盲人的意见,并使用m_ 而不是_

标签: c++ coding-style getter-setter


【解决方案1】:

对于长期存在的类来说,使用 getter 方法是一个更好的设计选择,因为它允许您在将来用更复杂的东西替换 getter 方法。尽管对于 const 值似乎不太可能需要此操作,但成本低且可能带来的好处很大。

顺便说一句,在 C++ 中,为成员同名的 getter 和 setter 是一个特别好的主意,因为将来您可以实际更改这对方法:

class Foo {
public:
    std::string const& name() const;          // Getter
    void name(std::string const& newName);    // Setter
    ...
};

到为每个定义一个operator()() 的单个公共成员变量中:

// This class encapsulates a fancier type of name
class fancy_name {
public:
    // Getter
    std::string const& operator()() const {
        return _compute_fancy_name();    // Does some internal work
    }

    // Setter
    void operator()(std::string const& newName) {
        _set_fancy_name(newName);        // Does some internal work
    }
    ...
};

class Foo {
public:
    fancy_name name;
    ...
};

客户端代码当然需要重新编译,但不需要更改语法!显然,这种转换同样适用于 const 值,其中只需要一个 getter。

【讨论】:

  • 嘿,我在发布我的问题之前找到了你的答案......它似乎几乎完全相同,而且你似乎提出了我正在寻找的解决方案类型,但作为一个菜鸟我不能确定。你介意看看我的问题吗:stackoverflow.com/questions/8454887/…,让我知道这个答案是否在我想要完成的正确轨道上?
  • 如果你和我一样困惑:没有operator()()。有一个带有参数列表的operator()
  • 用公共成员替换是否会破坏允许绕过 getter/setter 的封装?
  • @a1an:这个想法是公共成员不是像std::string这样的简单类型(可以由调用者以不可变的方式更改),而是某种类类型的对象(如我的示例 fancy_name 类)通过公开更有限的接口来保护所有必要的不变量本身。
【解决方案2】:

公开非常量字段往往不是一个好主意,因为这样会变得难以强制错误检查约束和/或在未来更改值时添加副作用。

在您的情况下,您有一个 const 字段,因此上述问题不是问题。将其设为公共字段的主要缺点是您锁定了底层实现。例如,如果将来您想将内部表示更改为 C 字符串或 Unicode 字符串或其他内容,那么您将破坏所有客户端代码。使用 getter,您可以转换为现有客户端的旧表示,同时通过新的 getter 向新用户提供更新的功能。

我仍然建议使用您上面放置的 getter 方法。这将最大限度地提高您未来的灵活性。

【讨论】:

  • 如果我从返回 std::string 的函数更改为返回一些 Unicode 字符串的函数,我已经破坏了所有客户端代码。
  • ..如果您的内部表示是 Unicode,但您可以转换为 UTF8 以与现有客户端兼容怎么办? getter 方法可以进行转换,但公共字段会禁止这种模式。
  • @JBRWilkinson 对我来说似乎是一个可能需要不同名称的 Unicode 和 UTF8 变体的 getter 的情况。如果内部表示保持不变,但 getter 类型发生了变化,那么您就破坏了兼容性;仅指出这一点是如果您认为内部表示会发生变化。
  • @Andy 为此类内容使用 gettor 的另一个很好的理由是,如果有自然的默认形式,但也有可选的其他形式。要使用不同的 8 位编码,可以选择 get_my_string() + get_my_utf8_string() + get_my_cp1252_string() 等,也可以选择 get_my_string(const char* encoding = "utf8")
  • @MrFooz 是的......但如果可以的话,我会返回类似 QString 的东西,它本身提供所有这些转换,这样我就不必为每个字段编写一堆 getter
【解决方案3】:

顺便说一句,在 C++ 中,有一个 const 引用成员有点奇怪。您必须在构造函数列表中分配它。谁拥有该对象的实际内存,它的生命周期是多少?

关于风格,我同意其他人的观点,即您不想暴露自己的隐私。 :-) 我喜欢 setter/getter 的这种模式

class Foo
{
public:
  const string& FirstName() const;
  Foo& FirstName(const string& newFirstName);

  const string& LastName() const;
  Foo& LastName(const string& newLastName);

  const string& Title() const;
  Foo& Title(const string& newTitle);
};

这样你可以做这样的事情:

Foo f;
f.FirstName("Jim").LastName("Bob").Title("Programmer");

【讨论】:

  • 虽然读写不错,但是方法链('fluent')API 风格会带来一些开销:stackoverflow.com/q/3134416/102345
  • @chrish 我使用 C++ 已经很多年了,但从未想过这样做。这种 getter/setter 样式的名称是什么?
  • @BufferOverflow 被称为 fluent api 风格。它与 Java builder 模式有关。
【解决方案4】:

我认为现在的 C++11 方法会更像这样。

#include <string>
#include <iostream>
#include <functional>

template<typename T>
class LambdaSetter {
public:
    LambdaSetter() :
        getter([&]() -> T { return m_value; }),
        setter([&](T value) { m_value = value; }),
        m_value()
    {}

    T operator()() { return getter(); }
    void operator()(T value) { setter(value); }

    LambdaSetter operator=(T rhs)
    {
        setter(rhs);
        return *this;
    }

    T operator=(LambdaSetter rhs)
    {
        return rhs.getter();
    }

    operator T()
    { 
        return getter();
    }


    void SetGetter(std::function<T()> func) { getter = func; }
    void SetSetter(std::function<void(T)> func) { setter = func; }

    T& GetRawData() { return m_value; }

private:
    T m_value;
    std::function<const T()> getter;
    std::function<void(T)> setter;

    template <typename TT>
    friend std::ostream & operator<<(std::ostream &os, const LambdaSetter<TT>& p);

    template <typename TT>
    friend std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p);
};

template <typename T>
std::ostream & operator<<(std::ostream &os, const LambdaSetter<T>& p)
{
    os << p.getter();
    return os;
}

template <typename TT>
std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p)
{
    TT value;
    is >> value;
    p.setter(value);
    return is;
}


class foo {
public:
    foo()
    {
        myString.SetGetter([&]() -> std::string { 
            myString.GetRawData() = "Hello";
            return myString.GetRawData();
        });
        myString2.SetSetter([&](std::string value) -> void { 
            myString2.GetRawData() = (value + "!"); 
        });
    }


    LambdaSetter<std::string> myString;
    LambdaSetter<std::string> myString2;
};

int _tmain(int argc, _TCHAR* argv[])
{
    foo f;
    std::string hi = f.myString;

    f.myString2 = "world";

    std::cout << hi << " " << f.myString2 << std::endl;

    std::cin >> f.myString2;

    std::cout << hi << " " << f.myString2 << std::endl;

    return 0;
}

我在 Visual Studio 2013 中对此进行了测试。不幸的是,为了使用 LambdaSetter 中的底层存储,我需要提供一个“GetRawData”公共访问器,这可能会导致封装损坏,但您可以将其省略并提供自己的T 的存储容器,或者只是确保您使用“GetRawData”的唯一时间是在您编写自定义 getter/setter 方法时。

【讨论】:

  • 这不是矫枉过正吗?这种方法使用了很好的封装,因此您只需要维护LambdaSetter 类,但从性能角度来看,我认为摆脱函数指针是个好主意?而且myString.SetGetter( 在我看来有点尴尬,因为它给对象一个函数,它能够执行给定的操作,即设置myString。该方法肯定朝着正确的方向发展,但我会明确地简化这一点,而无需 std::functions
  • 这肯定是矫枉过正,但它为您带来的是在运行时更改变量设置/获取方式的能力。摆脱 std::functions 会将其重新转换为在编译时确定的 set/get(可能对大多数应用程序都有好处)模板化的类可能会更好地服务,因此该类可以选择采用 set/get 方法和它将在编译时确定。
  • 我认为这是过早的优化。
  • 顺便说一句,我永远不会为此烦恼,试图实现 C# 在 C++ 中的原生功能是一件愚蠢的事情,但它确实回答了 OP 的问题。
【解决方案5】:

即使名称是不可变的,您可能仍希望选择计算它而不是将其存储在字段中。 (我意识到这对于“名称”来说不太可能,但让我们针对一般情况。)因此,即使是常量字段也最好包裹在 getter 中:

class Foo {
    public:
        const std::string& getName() const {return name_;}
    private:
        const std::string& name_;
};

请注意,如果您要更改 getName() 以返回计算值,则它无法返回 const ref。没关系,因为它不需要对调用者进行任何更改(模重新编译。)

【讨论】:

  • 所以让我重新总结一下这个答案:1)您极不可能更改getName() 的实现,因为...该死,数据只是存储在世界上您每次访问时都会重新计算它吗?您正在使用 C++ 来提高性能,不是吗?并且 2) 最后,即使您真的想出这样一个疯狂的用例,您实际上也无法做到这一点,因为在不触发未定义行为的情况下返回 const ref 是不可能的。你确定了吗?
  • ulidtko,根据你在这里的“语气”,我怀疑我给你的任何答案都能安抚你。但是,让我指出一件事:我写了“注意,如果您要更改 getName() 以返回计算值,它不能返回 const ref。没关系,因为它不需要任何更改给调用者(模重新编译。)”换句话说,您必须将方法从 const std::string&amp; getName() const 更改为 std::string getName() const。当然,更改内联函数或字段总是需要重新编译,所以这不会增加编译时的负担。
  • 您的注释是正确的。您可能也可以从一些智能指针中获利。然而,我确实对 C++ 中的 getter 和 setter 有意见,我正在收集支持和反对它的论据。
  • 关于 getter 的这个参数(即切换到重新计算值而不是返回存储的值的选项)我可以提出以下反对意见:几乎可以肯定,通过重新计算 getter 中的值,您将破坏合同。大多数客户端将依赖于 getter 的某些“直观”属性:它们速度快、不会抛出、不会分配、应该是线程安全的,等等。你可能会很幸运,一切仍然适用于你的不平凡的吸气剂,但编程不是靠运气。
  • 我同意你的观点,有时调用者可能依赖于 getter,其运行速度基本上与访问字段一样快。在某些圈子中,这被用作反对语言支持透明 getter 的论据。我对隐式性能合同的想法表示同情,但我认为也适用一些警告购买者:如果开发人员使用访问器,他们保留更改实现的权利。如果他们没有明确承诺恒定时间访问,那么您不应该编写假定恒定时间访问的代码。
【解决方案6】:

避免使用公共变量,除了本质上是 C 风格结构的类。这不是一个好习惯。

一旦定义了类接口,您可能永远无法更改它(除了添加它),因为人们会在它的基础上构建并依赖它。将变量公开意味着您需要拥有该变量,并且您需要确保它满足用户的需求。

现在,如果您使用 getter,您承诺会提供一些信息,这些信息当前保存在该变量中。如果情况发生变化,并且您不想一直维护该变量,则可以更改访问权限。如果需求发生变化(并且我看到了一些非常奇怪的需求变化),并且您主要需要此变量中的名称,但有时需要该变量中的名称,您只需更改 getter。如果您将变量公开,您将无法使用它。

这不会总是发生,但我发现写一个快速的 getter 比分析情况看看我是否会后悔将变量公开(并冒着以后出错的风险)要容易得多。

将成员变量设为私有是一个好习惯。任何有代码标准的商店都可能会禁止将偶尔的成员变量公开,任何有代码审查的商店都可能会因此批评你。

只要对写作的便利性真的不重要,就养成更安全的习惯。

【讨论】:

  • 我倾向于不同意,因为对于公共字段,“更改需求”的情况并不是那么致命:您仍然可以使用一些模板、继承和operator= 来模拟 C++ 中的属性。实际上大多数时候你不需要这个(说真的,你做了多少次重要的getter或setter?),而get()/set()的客户端语法很丑。
【解决方案7】:

从多个 C++ 源中收集想法并将其放入一个不错的、仍然非常简单的 C++ 中的 getter/setter 示例:

class Canvas { public:
    void resize() {
        cout << "resize to " << width << " " << height << endl;
    }

    Canvas(int w, int h) : width(*this), height(*this) {
        cout << "new canvas " << w << " " << h << endl;
        width.value = w;
        height.value = h;
    }

    class Width { public:
        Canvas& canvas;
        int value;
        Width(Canvas& canvas): canvas(canvas) {}
        int & operator = (const int &i) {
            value = i;
            canvas.resize();
            return value;
        }
        operator int () const {
            return value;
        }
    } width;

    class Height { public:
        Canvas& canvas;
        int value;
        Height(Canvas& canvas): canvas(canvas) {}
        int & operator = (const int &i) {
            value = i;
            canvas.resize();
            return value;
        }
        operator int () const {
            return value;
        }
    } height;
};

int main() {
    Canvas canvas(256, 256);
    canvas.width = 128;
    canvas.height = 64;
}

输出:

new canvas 256 256
resize to 128 256
resize to 128 64

您可以在这里在线测试:http://codepad.org/zosxqjTX

PS:FO 伊维特

【讨论】:

    【解决方案8】:

    来自设计模式理论; “封装变化的东西”。通过定义“吸气剂”,可以很好地遵守上述原则。因此,如果将来成员的实现表示发生变化,则可以在从“getter”返回之前对成员进行“按摩”;暗示在进行“getter”调用的客户端没有代码重构。

    问候,

    【讨论】:

    • 封装变化 — 翻译:“编写 getter 和 setter”。那种争论不买我。虽然通过属性(可以在 C++ 中模拟)解决了在不破坏客户端代码的情况下切换内部表示的问题,但 IMO 的 getter 和 setter 语法确实很丑陋,并迫使所有客户端编写丑陋的代码。所以我最好把成员变量设为public,尤其是const时。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-07
    • 2015-05-27
    • 2012-01-01
    • 1970-01-01
    • 2010-12-17
    • 2010-11-15
    相关资源
    最近更新 更多