C++11中的大部分setter函数是否应该编写为接受通用引用的函数模板?

67

考虑一个类 X,它有 N 个成员变量,每个变量都是可复制和可移动的类型,并且有 N 个对应的 setter 函数。 在 C++98 中,定义 X 的代码可能如下所示:

class X
{
public:
    void set_a(A const& a) { _a = a; }
    void set_b(B const& b) { _b = b; }
    ...
private:
    A _a;
    B _b;
    ...
};

X 的 setter 函数可以分别绑定左值和右值参数。根据实际参数,这可能会导致临时对象的创建,最终会导致复制赋值; 因此,此设计不支持 不可复制 类型。

使用 C++11 我们拥有了移动语义、完美转发和通用引用(Scott Meyers 的术语),它们允许通过以下方式重写 setter 函数,从而实现更高效和广泛的使用:

class X
{
public:
    template<typename T>
    void set_a(T&& a) { _a = std::forward<T>(a); }

    template<typename T>
    void set_b(T&& b) { _b = std::forward<T>(b); }
    ...
private:
    A _a;
    B _b;
    ...
};

通用引用可以绑定到const/非constvolatile/非volatile,以及一般情况下的任何可转换类型,避免创建临时对象并将值直接传递给operator=。现在支持了不可复制、可移动类型。可以通过static_assertstd::enable_if来消除可能不想要的绑定。

所以我的问题是:作为一种设计准则,C++11中所有(假设大多数)的setter函数都应该编写为接受通用引用的函数模板吗?

除了更繁琐的语法和在编写这些setter函数时无法使用类似Intellisense的辅助工具外,“尽可能编写接受通用引用的函数模板作为setter函数”的假设原则是否存在任何相关的缺点?


3
@sehe:这些setter函数故意保持简单,可能在里面发生的事情不仅仅是赋值。 - Andy Prowl
4
也许将来的实现不会实际上有一个名为a的字段,但仍会通过以其他方式存储从指定的A实例中所需的任何属性来逻辑地实现set_a。或者也许在将来,字段的值不会与所有其他数据成员正交,因此set_a也可能更新其他内容。我知道,YAGNI,但如果类被称为URL,即使我愿意始终拥有一个set_protocol成员函数,我也不一定希望承诺公开具有类型为stringprotocol数据成员。 - Steve Jessop
3
@sehe:我不明白你的观点。我可能有一个需要非平凡设置/获取的成员变量(我的示例只是简化,赋值可能只是其中的一部分)。为什么我不能为此编写getter/setter函数?当然,我在示例中没有显示getter函数,因为它们与我提出的问题无关,但这并不意味着这些属性是只能写入的。 - Andy Prowl
3
为什么在Stack Overflow上过于简单的例子和过于复杂的例子一样糟糕?我觉得你完全理解了这个想法。当我们想要通过getter/setter隐藏成员时,应该如何实现?原帖提供了一个小例子。就是这样。 - leemes
4
比如说以后触发一个 a_changed() 事件怎么样?或者调试一下属性的变化... - leemes
显示剩余14条评论
1个回答

37

你已经了解了类A和B,因此你知道它们是否可移动以及这个设计是否最终必要。对于像std::string这样的东西,如果你不知道在这里是否存在性能问题,改变现有代码是浪费时间的。如果你正在处理auto_ptr,那么现在是时候将其删除并使用unique_ptr了。

如果你没有更具体的信息,通常现在更喜欢按值传递参数-例如

void set_a(A a) { _a = std::move(a); }

这样允许使用 A 的任何构造函数,而不需要除了可移动性以外的其他要求,并提供一个相对直观的接口。


6
好的,我将接受这个答案,并把我正在寻找的设计原则表述如下:“对于像示例中所显示的设置函数,如果类型是可移动的且移动构造的性能不是问题,则按值传递参数;否则,使用通用引用,并通过std::enable_ifstatic_assert排除不想要的绑定;无论哪种情况,都要避免使用常量引用作为参数(再次强调:对于像示例中的设置函数)”。谢谢。 - Andy Prowl
19
对于像std::string这样的变量,如果按值传递参数,每次使用一个左值调用函数时都会导致内存分配。而通过const引用传递参数,真正的工作发生在函数体内的赋值操作中。由于真正的工作是在复制赋值而不是复制构造函数中进行的,如果预先分配的空间足够大,则可以重复使用它们。 您可以在循环中对此进行基准测试,您将看到const引用对于左值调用远优于按值传递参数。如果您想要rvalues的速度提升,应该实现一个rvalue重载。 - Nir Friedman
1
我对“传值”+std::move()是否是该问题的正确设计表示怀疑。如果是这样,为什么标准库中的许多类都提供了T const&T&&两种重载?我们为什么需要这两种重载呢? - Siu Ching Pong -Asuka Kenji-
“按值传递”相对于“右值引用”的优势在哪里? - ted
4
复制构造函数总是触发内存分配。只有在新内容显着大于旧内容时,复制赋值才会触发内存分配。即使假设内存分配“紧凑”,对于一个包含N个不同大小的字符串的列表,通过复制构造函数进行设置需要进行N次内存分配。而通过复制赋值则是O(logN)的时间复杂度。 - Nir Friedman
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接