C++ - 仅包含静态成员的类是否是不良实践/反模式?

3
关于仅包含静态成员的类,我看到有多个人声称这是一种不好的模式,而且它从来不是解决问题的最佳方案。 在C++中,只包含静态成员的实用程序类是否为反模式?上被接受并得到最高票数的答案主张使用命名空间,并总结道:
“对我来说,最明显的答案是:因为我们不需要面向对象来实现这个。”
这让我感到困惑,原因有几个。它似乎是在谈论仅包含静态成员函数的类,而不是包含静态数据和静态函数的类。仅包含静态成员函数的类确实可以用命名空间内的全局函数替换,但如何替换同时包含静态函数和静态数据的类呢?
struct Foo
{
    static void add5()
    {
        s_x += 5;
    }
    static const int& getX()
    {
        return s_x;
    }
private:
    static int s_x;
};
int Foo::s_x{ 0 };

在一个命名空间内有函数和全局变量?你如何确保s_x不能被外部代码直接访问和修改?
另一件让我困惑的事情是,我觉得只有静态成员的类在C++的书籍和库中是相当常见的。 SFMLsf::Mouse只有静态成员。 游戏编程模式多次使用只有静态成员的类,甚至在单例模式章节中用术语“静态类”来引用它。而服务定位器章节本质上就是一个只有静态成员的类。
那么应该怎么想呢?只有静态成员的类是不好的设计/不良实践吗?还是它们在某些情况下有其存在的意义?

3
可以找到一些不良设计的例子并不代表这是好的设计。 :-) 您可以将命名空间数据放在cpp文件中,并仅公开函数。您不必将其放在头文件中。 - BoP
3
我不会做出这样的笼统的“反模式”陈述。像 C++ 的许多方面一样,它可能被误用或正确使用。虽然命名空间可能类似,但你的 struct Foo 也是一种类型,这意味着它可以在类型上下文中使用,比如模板参数。 - Drew Dormann
我也不会将其描述为“反模式”。然而,有很多文章描述了为什么从设计或编码的角度来看,避免静态分配或“全局”数据通常更好 - 无论是未命名命名空间中的数据还是类的静态成员。但是,像任何编程语言的任何特性一样,有时静态分配数据是解决问题的合适甚至更优选的解决方案,而其他情况则有更好的选择。 - Peter
2个回答

3

其他问题的最佳答案仍然有效:您不需要使用类来封装静态函数和静态变量。这可以通过命名空间实现,而且只需一点点纪律,就可以使用编译单元的作用域。

在您的示例中,如果使用命名空间,则不需要公开私有静态数据成员的存在:

在头文件中声明:

namespace Foo {
    void add5();
    const int& getX();
}

然后在cpp中实现:

namespace Foo
{
    static int s_x{0};   // static globals are not visible outside the compilation unit

    void add5()
    {
        s_x += 5;
    }
    const int& getX()
    {
        return s_x;
    }
};

注意:静态全局变量并不完全等同于静态成员,但在上述代码中,效果相同,因为它是私有的。对于公共数据成员,您只需删除关键字 static 即可。

那么对于通常使用“静态类”来完成的任务,您建议完全放弃类,并使用具有静态全局变量和全局作用域函数的命名空间吗?例如,对于游戏的Engine类或AudioSystem类,只会有一个实例,并且将在代码中的许多地方使用。如果您要编写一个游戏,您会放弃这些类并将全局变量和函数放在audioengine命名空间中吗?还是您会为此编写普通的类,然后在头文件中声明全局静态实例? - JensB
@JensB 我的意思不是要不惜一切代价摆脱类,而是只在需要封装时使用类(请参见我在另一个答案中有关策略/策略模式的评论)。C++是多范式的,如果您只有一个引擎和一个带有全局状态的音频系统(因为这就是静态数据成员所表示的),那么使用类就没有优势了,并且使用命名空间会更加灵活(例如,组合命名空间或在不同版本之间进行选择)。 - Christophe
@JensB 我不在游戏行业,所以无法帮助你。但是在我的书架上,我有Mike McShaffry的《Game Coding Complete》。这本书真的很有趣。他了解游戏。所以我打开了关于物理引擎的页面:他使用一个具有虚函数和普通成员的真实类。这允许多态性和运行时配置,重用与游戏特定专业化相关的东西,这些都是您在仅具有静态成员的虚假类中所没有的。顺便说一句,如果您有不属于一起的功能,则人为地将它们打包到一个类中会违反SRP原则。 - Christophe

2

虽然我通常也推荐使用命名空间,但有些情况下类明显更好。例如,您可以将类作为模板参数传递,这意味着您可以以不同方式实现接口的策略类并“插入”它。使用命名空间无法做到这一点。

但是,如果您不打算以这种方式引用名称组,则我不建议将其作为组织代码的一般方法。


这是一个很有道理的观点。确实存在一些情况,只有静态成员的C++类是有意义的,而模板就是一个非常好的例子。然而,这主要与是否存在几个可替代的类(即策略模式、策略驱动设计)有关。我的回答主要针对实用函数的一般情况,特别是如果它们没有真正相关性。在这种情况下,命名空间甚至具有促进版本控制和使用语句进行命名空间组合的优势。 - Christophe
@Christophe:有人可能会说,“静态类”不会受到对using namespace XXX;的误用的困扰;-) - Jarod42

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