为什么我们需要静态成员函数,而自由函数可以做同样的事情呢?

15
在阅读了sbi和Eli Bendersky在这个问题中的回答之后,我开始思考静态成员函数的用途。
一个类的友元自由函数不应该能够做到静态成员函数所能做的任何事情吗?如果是这样,为什么/何时应该优先选择静态成员函数而不是友元自由函数呢?

1
通常情况下,坦率地说,你不应该这样做。 - Lightness Races in Orbit
5个回答

14

一般而言:

需要访问私有成员

静态成员函数可以访问类的私有成员。如果需要这样做,您可以使用静态成员函数。无论如何,在头文件中声明它以便给予访问权限,因此最好将其作为成员而不是友元函数。单例模式常常使用getInstance()方法作为静态成员函数,而使用静态工厂方法createInstance()的类则确保它们在堆上创建。这两种情况都需要访问私有构造函数。

元编程

静态成员函数非常适合模板元编程,您可以传递一个类并调用其方法,而在调用点并不知道实际调用哪个函数。这通常被称为“编译时多态”,是元编程的重要部分。std::char_traits就是基于这个原理的。

受限访问

仅为了使其仅可由类访问而使用私有静态成员函数的常见方式,而它本身不需要访问私有成员,这不是静态成员函数的良好使用,因为它是类实现细节的一部分,最好在编译单元的匿名命名空间中完成。

但是,如果静态成员函数是受保护的,那么它有用,因为它可以被派生类调用,但不可被外部类调用。

友元函数

  • 可以访问私有成员,但需要在头文件中声明。
  • 可以作为“重载”的一部分在元编程中使用,但仍然需要在头文件中声明。(常见的例子是operator<<
  • 在友元关系中无法限制方法的受保护访问,因为这里想要限制的是方法的访问权限而不是调用方的访问权限。

在模板元编程中,哪些情况下不能使用友元自由函数?我找不到任何例子,其中一个带模板的友元自由函数无法做到静态成员函数所能做的事情。第三点很好,我没有想到! - peoro
特质类(traits classes)例如。traits_class::dostuff( params ); 其中dostuff是traits_class的静态成员。 - CashCow
在上面的答案中,traits_class 是一个模板参数或从模板参数派生而来。 - CashCow
我认为这也可以使用自由函数完成(即:dostuff< traits_class >(params);),但这样做会更糟,因为 dostuff 不在 traits_class 范围内。 - peoro
1
@peoro:我不太确定,自由函数的一个优点是,如果用户不想提供特征,你可以轻松地提供默认版本。在特征类中测试静态成员函数的存在更加困难。 - Matthieu M.
当然可以创建一个重载的友元函数,用于元编程。operator<<是一个常见的例子。在模板中,您可以根据存在的重载流式传递模板参数。在这种情况下,它必须是非成员函数,但由于必须在头文件中声明,所以它不会增加可扩展性,因此我认为如果可能的话,静态函数更可取。 - CashCow

12

静态方法:

  • 提供了类创建的 "命名空间" 中的封装。如果您的类是 Animal ,而静态方法是 Create,则必须使用 Animal::Create 来调用它。这比全局函数更好,并允许使用相对自然的语法实现工厂和 "虚构造函数"。
  • 可以访问静态成员。这些成员在某些情况下非常有用,没有静态方法和成员,您将不得不使用全局变量和函数。

1
此外,如果给定类的实例,它们也可以访问所有私有方法和函数。这非常有用。 - Macke
静态类成员也可以在自由函数中访问,而友元自由函数可以访问非公共方法。你对第一个观点是正确的,但这只是语法问题,不是吗?这是否与其他问题中所说的相矛盾? - peoro
@peoro: (1) 主要是关于命名空间,而不是语法。在类内部拥有一个静态方法是很方便的。否则,你如何逻辑上将其与类相关联呢?一旦你有了(1),访问静态成员的更方便和“正常”的方式就是使用静态成员,而不是在类内声明全局函数并将它们声明为友元,我个人认为。 - Eli Bendersky
关于 1,Sutter一直倡导使用非友元非成员函数,并且这与其定义相呼应,即类的公共接口由类本身以及在其头文件中提供的所有自由函数组成。 - Matthieu M.
我实际上更喜欢使用工厂方法而不是静态创建方法。您可以创建一个基类友元工厂,其他工厂可以从它派生。虽然它们不能直接访问类,但基类友元工厂执行实际的创建操作,派生类则收集用于执行此操作的成员。 - CashCow
我上一条评论的典型例子是,当你从某个源中流式传输值时。工厂的不同实现将使用不同的格式,例如文本流、XML、数据库读取、协议缓冲区、boost序列化或其他。无论哪种方式,它们都会收集数据成员并调用友元工厂类成员来进行创建。 - CashCow

4

通常情况下,说实话,您不应该使用静态成员函数。免费的函数被极大地低估了。

使用静态成员(假装类只是静态成员的命名空间,这在某种程度上是正确的)获得的隐式“命名空间”是我能想到的唯一好处。

如果静态函数成员需要持久变量,则还可以使用静态数据成员。


3

有些人对使用静态函数持怀疑态度,因为它通常被那些来自过程化背景并不理解OO的人使用。

然而,有很多设计模式是使用静态成员函数实现的,这是有意义的。

例如,单例模式和工厂模式就是其中的两个顶级模式,实际上,大多数需要对象创建的结构模式都需要静态成员函数。


1
是的,静态函数用于此目的,但友元自由函数应该能够在没有任何劣势的情况下完成相同的任务... - peoro
但是也要注意全局数据通过单例模式进行伪装。在这里可以看到关于此话题的丰富讨论:https://dev59.com/YXVC5IYBdhLWcg3w9GA9… – rholmes 刚刚编辑 - rholmes

1

如你所说,static成员函数没有增加任何价值。更糟糕的是,当用于实现细节时,这会引入额外的依赖关系(在编译方面)。

唯一无法使用匿名自由函数模拟的用途是protected访问,即派生类访问父静态函数。然而,这从未是必需的:您可以将其改为常规成员函数(我假设您没有全局状态,否则静态/友元区别不是立即关注的问题)。

在模板元编程中使用static函数已被提出...但它与内部类型问题非常相似:它使提供默认版本变得困难。另一方面,一个合适定义的自由函数(以指针形式接受类型),可以提供一个模板版本:

struct some_traits
{
  static void doStuff();
};

// versus

struct some_traits {};

void doStuff(some_traits*);

// and the default: void doStuff(...);

当然,总会有一个问题,为什么这应该是一个静态函数,而成员函数会为用户提供更多的灵活性。为此,我会引用标准委员会与Allocator概念一起采取的措施:现在已经授权使用有状态的分配器,这使我们有机会将给定map的节点打包在同一页中,而不是将它们散布在整个堆中。
最后,还有一个接口问题。然而,自从Sutter提倡一个类和在同一个头文件中定义的自由函数都构成了这个类的公共接口,也就是ADL的作用!所以这更多的是安慰古老的OO程序员,而不是“好的实践”。
实际上,我没有看到使用static成员函数的任何好处。我希望人们能想到相反的情况来提出真正的案例。

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