静态、非成员或静态非成员函数?

10

每当我有一些“实用型”功能时,我总会想知道哪个选项最好。例如,在打印消息结构(自己的或外部的)、一些编码/解码代码或者在我正在处理的上下文中仅使用几个有用的转换函数。

我考虑的选项有:

1)helper类/结构体中的静态函数。

struct helper
{
    static bool doSomething(...);
};

2) 非成员函数。

namespace helper
{
    bool doSomething(...);
}

3) 静态非成员函数。

namespace helper
{
    static bool doSomething(...);
}
在某些情况下,可能需要在“实用程序”中初始化或保持状态,因此我选择选项1以避免“全局”状态。然而,如果没有需要保留的状态,我应该选择选项2还是3?选项2和3之间的实际差异是什么?
重要的是考虑什么,并且有一种首选的方法来解决这个问题吗?谢谢!

选项(3)只是使函数局限于翻译单元,这可能并不是非常有用,因为您基本上只能在定义它的TU内部使用该函数。[另请参阅此问题](https://dev59.com/lG025IYBdhLWcg3wYFCH)。 - Kerrek SB
3个回答

14
选项2和选项3之间的区别在于,在第二种情况下,函数将是翻译单元内部的。如果函数仅在cpp中定义,则应选择此选项(这大致相当于一个未命名的命名空间--这是第四种要考虑的选项,再次大致相当于选项3)。
如果要让函数由不同的翻译单元使用,则应选择选项2。该函数将被编译一次(除非您将其标记为inline并在头文件中提供定义),而使用选项3时,编译器将在每个翻译单元中创建其自己的内部副本。
至于选项1,我会避免使用它。在Java或C#中,您被迫在所有地方使用类,并且当操作不能很好地映射到对象范例时,您最终会得到utility classes。另一方面,在C++中,您可以将这些操作作为自由站立函数提供,而无需添加额外的层次。如果选择utility class,请勿忘记禁用对象的创建。
无论函数是在类级别还是命名空间级别,都会影响查找,这将对用户代码产生影响。静态成员函数始终需要用类名限定,除非您在类作用域内,而有不同的方法将命名空间函数引入作用域。作为一个说明性的例子,请考虑一堆数学助手函数和调用代码:
double do_maths( double x ) {
   using namespace math;
   return my_sqrt( x ) * cube_root(x);
}
// alternatively with an utility class:
double do_maths( double x ) {
   return math::my_sqrt(x) * math::cube_root(x);
}

哪一个更容易阅读取决于个人,但我更喜欢前者:在函数内我可以选择命名空间,然后只关注操作而忽略查找问题。


+1 对此表示赞同,但我更喜欢前者:在函数内部,我可以选择命名空间,然后只需专注于操作并忽略查找问题。重要的一点! - Nawaz

10

不要把类作为命名空间,使用自由函数(即非成员函数)作为命名空间中的最简单方法。

此外,如果该函数需要在多个翻译单元中使用,则不应将其设置为静态。否则,它会被复制。

将类用作命名空间是愚蠢的:为什么要创建一个您不会实例化的类?

如果需要保留某些状态,则可以在某个地方保留全局状态:函数内的静态变量、实用程序全局对象(可能位于“私有”命名空间中或位于其中一个翻译单元的静态全局变量中)。不要编写不需要实例化的类。

遗憾的是,具有C#/Java背景的人习惯于做这种愚蠢的事情,但这是因为他们的语言设计人员已经单方面决定自由函数是邪恶的。他们是否应该被开除是一种宗教问题。

最后要提醒的是:应该很少使用全局状态。实际上,当代码增长时,它会以一种您无法控制的方式将您的代码与全局状态联系起来。您应该始终问自己为什么不使这种联系明确。


当然可以。这仍然是一个选择,特别是如果需要保持状态。 - murrekatt
1
+1 对此的看法因信仰而异,是否应该开枪就不好说了。 :D - Nawaz
阅读您的回答,看起来我按了一个按钮。 - murrekatt
1
@murrekatt:静态成员变量和其他全局变量一样。您可以控制对状态的访问权限,但可以通过不同的方式实现,例如将函数和状态封装在自己的翻译单元中,并使用仅提供内部声明为“static”或未命名命名空间的头文件。这样做的优点是真正隐藏了实现细节:如果更改这些变量,则只需要编译自己的翻译单元,因为它们不会在任何其他地方看到/包含。 - David Rodríguez - dribeas

-2

我使用具有静态函数的结构体,因为它比名称空间中的非成员函数提供了稍微更好的隔离(由于避免了Koenig查找)。

为了举例说明我想要避免的事情:

namespace Foo
{
      struct A
      {
      };

      void f(const A& a) {}
}

void f(const Foo:A& a) { std::cout << "AAA"; }

int main(void)
{
      Foo::A a;
      f(a); // calls Foo:f
      return 0;
}

1
我使用自由函数,因为存在Koenig查找。 - Alexandre C.
取决于你想做什么。 - quant_dev
1
如果你能提供一个例子,证明1)Koenig查找会对你造成伤害,2)Koenig查找可能会对你造成伤害并不是一种设计上的问题,我很愿意接受你的观点。我无法看出问题所在:毕竟,Koenig查找通常是一种优势。 - Alexandre C.
嗯,我必须费心去关心你做这件事或那件事的准备情况。但我并不在意;-) - quant_dev
1
这只是一个建议,旨在增强您简洁的陈述。至于您实际的示例,通过编写void f(const A& a),您承认fA接口的一部分,我认为作为一个特性Foo::f被调用而不是来自全局命名空间的f(您不会将任何内容放入全局命名空间,对吗?)。我无法看到潜在的歧义应该存在的地方。 - Alexandre C.

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