【问题标题】:Namespaces vs. Static Classes命名空间与静态类
【发布时间】:2011-12-09 18:35:20
【问题描述】:

对于我正在进行的一个项目,我有一堆“库类”。这些本质上是价值相关功能的集合。其中一些库需要在运行时“初始化”。到目前为止,我一直在使用以下设计作为解决方案:

// Filename: Foo.h
namespace my_project
{
namespace library
{
class Foo
{
public:
    static int some_value; // members used externally and internally

    Foo()
    {
        // Lots of stuff goes on in here
        // Therefore it's not a simply member initialization
        // But for this example, this should suffice
        some_value = 10;
        Foo::bar();
    }

    static void bar() { ++some_value; } // some library function

    // no destructor needed because we didn't allocate anything

private:
    // restrict copy/assignment
    Foo(const Foo&);
    void operator=(const Foo&);
};
int Foo::some_value = 0; // since some_value is static, we need this
} // library namespace
static library::Foo Foo;
} // my_project namespace

使用Foo 与此类似,例如:

#include "Foo.h"
using namespace my_project;
int main()
{
    int i = Foo.some_value;
    Foo.bar();
    int j = Foo.some_value;
    return 0;
}

当然,这个例子非常简单,但它抓住了重点。这种方法对我来说有四个好处:

  1. 库的用户无需担心初始化。他们不需要在main() 中调用类似Foo::init(); 的东西,因为library::Foo 是在构造my_project::Foo 时初始化的。这是这里的主要设计约束。 用户应该负责初始化库。

  2. 我可以在库中创建各种私有函数来控制它的使用。

  3. 用户可以出于任何原因选择创建此库的其他实例。但不允许复制。默认情况下会为他们提供一个实例。这是一项要求。

  4. 我可以使用. 语法代替::。但这是个人风格。

现在的问题是,这个解决方案有什么缺点吗?我觉得我在做 C++ 不应该做的事情,因为 Visual Studio 的 IntelliSense 一直在吓坏我,并认为 my_project::Foo 没有被声明。可能是因为对象和类都被称为Foo,即使它们位于不同的命名空间中?

解决方案编译良好。我只是担心一旦我的项目规模变大,我可能会开始出现名称歧义。此外,我创建这个库的对象是否浪费了额外的内存?

我应该坚持使用singleton design pattern 作为替代解决方案吗?有其他解决方案吗?

更新:

在查看了所提供的解决方案并在 google 上搜索各种解决方案后,我偶然发现了extern。我不得不说我对这个关键字的真正作用有点模糊。自从我学习 C++ 以来,我一直对此感到困惑。但是在调整了我的代码之后,我把它改成了这样:

// Foo.h
namespace my_project
{
namespace library
{
class Foo_lib
{
public:
    int some_value;
    Foo_lib() { /* initialize library */ }
    void bar() { /* do stuff */ }
private:
    // restrict copy/assignment
    Foo_lib(const Foo_lib&);
    void operator=(const Foo_lib&);
};
} // library namespace
extern library::Foo_lib Foo;
} // my_project namespace

// Foo.cpp
#include "Foo.h"
namespace my_project
{
namespace library
{
    // Foo_lib definitions
} // library namespace
library::Foo_lib Foo;
} // my_project namespace

// main.cpp
#include "Foo.h"
using namespace my_project;
int main()
{
    int i = Foo.some_value;
    Foo.bar();
    int j = Foo.some_value;
    return 0;
}

这似乎与之前的效果完全相同。但正如我所说,由于我对 extern 的使用仍然很模糊,这是否也会产生完全相同的不良副作用?

【问题讨论】:

  • 不,这里不需要单例。你为什么要这样做?
  • @R.MartinhoFernandes:因为库在使用前需要初始化到某个状态。并且用户不应该负责初始化它。
  • @teedayf: 如果他们自己想在静态初始化期间使用它会发生什么?

标签: c++ visual-studio-2010 namespaces


【解决方案1】:

这一行特别不好

static library::Foo Foo;

它会在每次翻译中发出Foostatic 副本。不要使用它 :) Foo::some_value 的结果将等于 Foo.h 可见的翻译数量,并且它不是线程安全的(这会让您的用户感到沮丧)。

此行在链接时会导致多个定义:

int Foo::some_value = 0;

单身人士也很糟糕。在这里搜索@SO 会产生很多避免它们的理由。

只需创建普通对象,并向您的用户记录为什么他们应该在使用您的库时共享对象,以及在哪些情况下。

库的用户不需要担心初始化。他们不需要调用像 Foo::init(); 这样的东西。在他们的 main() 中,因为 library::Foo 是在构建 my_project::Foo 时初始化的。这是这里的主要设计约束。用户不应负责初始化库。

对象应该能够根据需要构建自己,而不会引入不可剥离的二进制包袱。

我可以在库中创建各种私有函数来控制它的使用。

这不是您的方法所独有的。

用户可以出于任何原因选择创建此库的其他实例。但不允许复制。默认情况下会为他们提供一个实例。这是一个要求。

然后您可以强制您的用户传递 Foo 作为必要参数来创建他们所依赖的类型(需要 Foo)。

我可以使用 .语法而不是 ::。但这是个人风格。

不好。不是线程安全的,然后用户可能会严重破坏您库的状态。私人数据是最好的。

【讨论】:

  • 如果我这样做,我认为只实现Foo::init() 并让用户调用它会更简单。或者有一个 init() 函数来初始化所有这些库。我认为那些会不那么混乱。不过,我宁愿满足要求。我完全明白为什么这条线现在不好了。
  • @teedayf Foo::init 可以移动到(或被调用)Foo 的构造函数。如果您记录“嘿,这个字体管理器 (Foo) 需要很长时间来构建并且需要大量内存”,那么您的用户应该听。他们也可能有完全正当的理由来创建多个字体管理器。静态初始化是一件痛苦的事,而且在事后消除这种依赖关系可能非常困难。暴露获取字体需要字体管理器(这是一个沉重的对象)这个事实确实不是一件坏事。
  • 我在谷歌搜索时偶然发现了另一种解决方案。我更新了原始问题。如果你给我你的想法,将不胜感激。 :)
  • @teedayf 更新只解决了两个问题,还有许多重要问题。命名空间中的extern 类似于类中的静态(因为只能有一个定义)。更新后的版本仍然存在严重问题——许多有经验的开发人员会竭尽全力避免(或不引入)。 Matthieu M. 还提出了重要的观点,更新没有解决:你仍然不应该使用单例,你应该使用私有数据,你应该允许你的用户正确初始化和销毁​​(阅读:当他们选择时),你的库应该是线程安全的。
  • 我明白你的意思。再次查看 Matthieu M 的解决方案,我认为这是我在保持“安全”的同时能得到的最接近的东西。这基本上就是我想知道的:staticextern 有什么不同。非常感谢所有的帮助。
【解决方案2】:

这里发生了两件事:

  • 如果用户非常想并行化她的代码怎么办?
  • 如果用户想在静态初始化阶段开始使用您的库怎么办?

所以,一次一个。

1.如果用户非常想并行化她的代码怎么办?

在多核处理器时代,库应该争取重入。 全局状态不好,不同步全局状态更糟。

我只是建议您让Foo 包含常规属性而不是static 的属性,然后由用户决定应该并行使用多少个实例,并可能选择一个。

如果将Foo 传递给您的所有方法会很尴尬,请查看Facade 模式。这里的想法是创建一个Facade 类,该类使用Foo 进行初始化,并为您的库提供入口点。

2.如果用户想在静态初始化阶段开始使用你的库怎么办?

静态初始化命令惨败简直太可怕了,静态销毁命令惨败(它的兄弟)也好不到哪里去,而且更难追踪(因为那里的内存不是0初始化的,所以很难看到发生了什么开)。

由于您再次很难(不可能?)预测库的使用情况,并且由于在静态初始化或销毁期间使用它的任何尝试都几乎不可能使用您创建的单例,所以更简单的做法是至少将初始化委托给用户。

如果用户不太可能愿意在启动和关闭时使用此库,那么您可以提供一个简单的保护措施,以便在首次使用时自动初始化库(如果她还没有)。

这可以通过使用局部静态变量以线程安全的方式 (*) 轻松完成:

class Foo {
public:
  static Foo& Init() { static Foo foo; return foo; }

  static int GetValue() { return Init()._value; }

private:
  Foo(): _value(1) {}
  Foo(Foo const&) = delete;
  Foo& operator=(Foo const&) = delete;

  int _value;
}; // class Foo

请注意,如果您只是决定使用单例并采用第一个解决方案:常规对象,仅具有每个实例的状态,那么所有这些粘合都是完全无用的。

(*) C++11 保证线程安全。在 C++03(业界主要使用的版本)中,最好的编译器也能保证这一点,如果需要,请查看文档。

【讨论】:

  • 我关注了您帖子的第一部分,并在没有static 的情况下定期声明了成员。但是,我还使用 extern 关键字与另一个解决方案合并。我仍然不确定该解决方案是否消除了副作用,所以我更新了问题。
  • @teedayf:你仍然有初始化/销毁顺序的问题(只要你保留一个全局对象,你就会有它)。
【解决方案3】:

现在的问题是,这个解决方案有什么缺点吗?

是的。例如,请参阅静态初始化命令失败的 c++ 常见问题解答中的此条目。 http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.14 tldr?本质上,您无法控制静态对象(例如上面的 Foo)的初始化顺序,任何关于顺序的假设(例如,用另一个静态对象的值初始化一个静态对象)都将导致未定义的行为。

在我的应用中考虑这段代码。

#include "my_project/library/Foo.h"

static int whoKnowsWhatValueThisWillHave = Foo::some_value;

int main()
{
   return whoKnowsWhatValueThisWillHave;
}

这里不能保证我从 main() 返回的内容。

用户可以出于任何原因选择创建此库的其他实例。但不允许复制。默认情况下会为他们提供一个实例。这是一个要求。

不是真的,不是...由于您的所有数据都是静态的,因此任何新实例本质上都是指向相同数据的空壳。基本上,你有一个副本。

我觉得我在做 C++ 不应该做的事情,因为 Visual Studio 的 IntelliSense 一直在吓坏我,并认为 my_project::Foo 没有被声明。可能是因为对象和类都被称为 Foo,即使它们位于不同的命名空间中?

你是!假设我将这一行添加到我的代码中:

使用命名空间 ::my_project::library;

“Foo”现在解析为什么?也许这是在标准中定义的,但至少是令人困惑的。

我可以使用 .语法而不是 ::。但这是个人风格。

不要与语言抗争。如果您想使用 Python 或 Java 语法编写代码,请使用 Python 或 Java(或 Ruby 或其他)...

我应该坚持使用单例设计模式作为替代解决方案吗?有其他解决方案吗?

是的,Singleton 是一个不错的选择,但您还应该考虑这里是否真的需要一个单例。由于您的示例只是句法,很难说,但也许使用依赖注入或类似的东西来最小化/消除类之间的紧密耦合会更好。

希望我没有伤害你的感受 :) 提问很好,但显然你已经知道了!

【讨论】:

  • 我真的很喜欢你的回复。你提出了我忽略的几点。我在示例中试图强调的主要内容是库需要在使用之前初始化到某个状态,用户不必担心手动初始化它。因此我需要一个构造函数。这就是为什么我认为单例会很有用。对吗?
  • 关于单例模式要记住的重要一点是,您正在创建一个只需要实例化一次的对象。如果该约束不重要,我认为依赖注入至少值得考虑。传递一组例程可能看起来很奇怪,但它增加了很大的灵活性。例如,当您想要进行测试驱动开发时,DI 通常很有用。也就是说,使用单例模式的延迟初始化可能是一个不错的方法,并且只需要很少的结构更改。例如。 Foo.SomeMethod() 变为 Foo::Instance().SomeMethod();
猜你喜欢
  • 2012-08-10
  • 2010-09-14
  • 2018-04-16
  • 1970-01-01
  • 2010-11-28
相关资源
最近更新 更多