在 C++ 中创建全局“常量”的正确方法

Posted

技术标签:

【中文标题】在 C++ 中创建全局“常量”的正确方法【英文标题】:Proper way to make a global "constant" in C++ 【发布时间】:2010-07-06 19:30:06 【问题描述】:

通常,我定义一个真正的全局常量(比如说 pi)的方式是将一个外部常量放在头文件中,然后在 .cpp 文件中定义常量:

常量.h:

extern const pi;

常量.cpp:

#include "constants.h"
#include <cmath>
const pi=std::acos(-1.0);

这对于真正的常数(例如 pi)非常有效。但是,在定义“常量”时,我正在寻找一种最佳实践,因为它将在程序运行之间保持不变,但可能会根据输入文件而改变。这方面的一个例子是引力常数,它取决于所使用的单位。 g 在输入文件中定义,我希望它是任何对象都可以使用的全局值。我一直听说拥有非常量全局变量是不好的做法,所以目前我将 g 存储在一个系统对象中,然后将其传递给它生成的所有对象。然而,随着对象数量的增加,这似乎有点笨重且难以维护。

想法?

【问题讨论】:

为什么每个对象都需要访问引力常数?只有少数模块应该关心这一点,那么为什么要如此广泛地使用它呢? @jalf- 系统对象生成的每个(或几乎每个)对象都需要该对象本地计算的引力常数。 真的吗?我可以理解为什么 physics 对象 可能 需要它(尽管即使那样我也可能会尝试将重力计算单独考虑为这些物理对象可以使用的服务,因此它们不需要自己知道重力)。但是您的程序不仅仅包含物理对象。当然,你还有其他课程。渲染器需要知道重力吗?你的输入处理吗?记录器? :) 这是一个难题——这是一个仅限 CLI 的学术物理模拟器。所以除了一些用于输出数据的记录器对象外,其余的对象都是物理对象。 【参考方案1】:

这完全取决于您的应用程序大小。如果您确实绝对确定某个特定常量在一次运行中将具有由代码中的所有线程和分支共享的单个值,并且将来不太可能改变,那么全局变量与预期的语义最接近,所以最好只使用它。如果需要,稍后重构它也是微不足道的,特别是如果您为全局变量使用独特的前缀(例如g_),这样它们就不会与本地变量发生冲突 - 这通常是一个好主意。

总的来说,我更喜欢坚持 YAGNI,不要一味安抚各种编码风格指南。相反,我首先查看他们的基本原理是否适用于特定案例(如果编码风格指南没有基本原理,那是一个糟糕的),如果它显然不适用,那么没有理由应用该指南那个案例。

【讨论】:

为什么不把它放在一个单独的命名空间而不是g_前缀? @jalf,更容易搜索 "g_something" 或 namespacename::something 和/或 using namespace namespacename; * 不同文件中的代码行 *\ 什么?命名空间非常适合消除歧义和防止冲突,但它们不是很容易搜索。 到目前为止,我是唯一一个为这个模拟器编写代码的人。它的编写方式使得其他人可以开始扩展它,并希望避免某些模块编写者改变引力常数(换句话说 - 我想要我的蛋糕并且也想吃它) @David:但@Pavel 使用名称分类作为该前缀的原因。他没有谈论可搜索性。如果您想要可搜索性,请不要使用using namespace。搜索globals::,您将捕获globals 命名空间中的所有内容。很容易搜索,我会说。我在搜索std::vector 的实例时也没有遇到任何问题。 @jalf:不,关键在于可搜索性。我在某种意义上提到了名称冲突,因为您有一个本地阴影全局 - 不是正常意义上的冲突,而是当您尝试进行查找和替换时的“冲突”。使用这样的命名空间也没有问题,只要您可以确保团队中的每个人都将始终完全限定其中的所有名称,而无需编写 usingusing namespace。使用g_,这对所有客户端都有效。【参考方案2】:

我能理解你的困境,但很遗憾你没有做对。

单元不应影响程序,如果您尝试在程序的核心处理多个不同的单元,您将受到严重伤害。

从概念上讲,您应该这样做:

       Parse Input
            |
 Convert into SI metric
            |
       Run Program
            |
Convert into original metric
            |
      Produce Output

这可确保您的程序与现有的各种指标很好地隔离开来。因此,如果有一天您以某种方式添加对 16 世纪法国公制系统的支持,您只需添加以正确配置 Convert 步骤(适配器),也许还有一些输入/输出(以识别它们并打印它们是正确的),但程序的核心,即计算单元,将不受新功能的影响。

现在,如果您要使用一个不太恒定的常数(例如地球上的重力加速度,它取决于纬度、经度和高度),那么您可以简单地将其作为参数传递,与其他参数组合在一起常量。

class Constants

public:
  Constants(double g, ....);

  double g() const;

  /// ...
private:
  double mG;

  /// ...
;

这可以设为Singleton,但这与(有争议的)依赖注入习语背道而驰。就我个人而言,我尽可能地远离Singleton,我通常使用一些Context 类,我在每个方法中传递,这样可以更轻松地相互独立地测试这些方法。

【讨论】:

【参考方案3】:

单例的合法使用!

带有设置单位方法的单例类常量()?

【讨论】:

合法吗?怎么会这样?因为有人想要一个全球性的? 单例相对于全局有什么好处? @Mark:嗯,这是一种设计模式,呵呵!你还需要什么? ;) @Mark Ransom - 如果你有“几乎”常量 - OP 想要切换单位并取回 g 的值(9.8m/s^s 或 32 ft/s^2)。拥有一个单例类和一个 units() 设置器可能比 global_imperial_g 和 global_metric_g 以及计算中的许多 if 语句更干净 我不喜欢单身人士。它们使测试等事情变得更加混乱,您需要在不同的配置上进行测试(例如在这种情况下),或者每个新测试都需要一个原始的单例。这当然是可行的,但在大多数情况下,我仍然认为它们很糟糕。例如,它们使测试套件无法并行化。【参考方案4】:

您可以使用后一种方法的变体,创建一个包含所有这些变量的“GlobalState”类并将其传递给所有对象:

struct GlobalState 
  float get_x() const;

  float get_y() const;

  ...
;

struct MyClass 
  MyClass(GlobalState &s)
  
    // get data from s here
    ... = s.get_x();
  
;

如果您不喜欢全局变量,它会避免使用它们,并且会随着需要更多变量而优雅地增长。

【讨论】:

【参考方案5】:

让全局变量在运行的生命周期内改变值是不好的。

在启动时设置一次的值(此后保持“恒定”)对于全局来说是完全可以接受的。

【讨论】:

【参考方案6】:

为什么您当前的解决方案难以维护?随着对象的增长,您可以将对象拆分为多个类(一个对象用于模拟参数,例如您的引力常数,一个对象用于一般配置等)

【讨论】:

最终可能不难维护,但是随着这段代码的增长,似乎变得越来越麻烦。我现在正在研究的问题是,“属于”系统对象的给定对象可能在其下具有几层对象,这些对象也需要引力常数。我刚刚遇到了一个问题,其中这些子子对象之一无法访问引力常数,并且必须通过我的层次结构返回并向下传递 g。 但是有多少次它使您免于添加不必要的依赖项?如果它们是真正的全局值,你会盲目地访问这些值多少次,你当前的解决方案迫使你停下来询问当前对象是否真的应该依赖这些值,导致在更清洁、更松散耦合的解决方案中?【参考方案7】:

对于具有可配置项的程序,我的典型习惯用法是创建一个名为“configuration”的单例类。在configuration 内部,可能会从解析的配置文件、注册表、环境变量等中读取内容。

通常我反对创建get() 方法,但这是我的主要例外。如果必须在启动时从某处读取配置项,通常不能将它们设为 consts,但您可以将它们设为私有并使用 const get() 方法将它们的客户端视图设为 const。

【讨论】:

不使用get的方法,为什么不把整个配置对象const除了需要初始化的地方之外呢? 很可能是因为我对这种技术一无所知。我知道您可以在初始化程序中初始化 const 成员,但这仅适用于相当简单的事情。 ...您是否可能建议使(隐藏)对象本身成为变量,但是让返回对它的引用的 static 方法返回它的 const 视图?下次它出现时,我必须尝试一下。【参考方案8】:

这实际上让人想起 Abrahams 和 Gurtovoy 撰写的 C++ 模板元编程一书 - 有没有更好的方法来管理您的数据,这样您就不会在从码到米或从体积到长度的转换中遇到糟糕的转换,也许还有那个类知道重力是一种形式的加速度。

另外你已经有一个很好的例子,pi = 某个函数的结果...

const pi=std::acos(-1.0);

那么为什么不将重力作为某个函数的结果,而该函数恰好是从文件中读取的呢?

const gravity=configGravity();

configGravity() 
 // open some file
 // read the data
 // return result

问题在于,由于全局是在调用 main 之前管理的,因此您无法向函数提供输入 - 什么配置文件,如果文件丢失或其中没有 g 怎么办。

因此,如果您想要进行错误处理,则需要稍后进行初始化,单例更适合。

【讨论】:

【参考方案9】:

让我们详细说明一些规格。所以你要: (1) 保存全局信息(重力等)的文件比您使用它们运行的​​可执行文件的寿命更长; (2) 在所有单位(源文件)中可见的全局信息; (3) 一旦从文件中读取,您的程序就不允许更改全局信息;

嗯,

(1) 建议对全局信息进行包装,其构造函数采用 ifstream 或文件名字符串 reference(因此,该文件必须存在 在调用构造函数之前并且调用析构函数后它仍然存在);

(2) 建议包装器的全局变量。此外,您可以确保这是此包装器的 only 实例,在这种情况下,您需要按照建议将其设为单例。再说一次,你可能不需要这个(你可以拥有相同信息的多个副本,只要它是只读信息!)。

(3) 建议来自包装器的 const getter。因此,示例可能如下所示:

#include <iostream>
#include <string>
#include <fstream>
#include <cstdlib>//for EXIT_FAILURE

using namespace std;

class GlobalsFromFiles

public:
    GlobalsFromFiles(const string& file_name)
    
        //...process file:
        std::ifstream ginfo_file(file_name.c_str());
        if( !ginfo_file )
        
            //throw SomeException(some_message);//not recommended to throw from constructors 
            //(definitely *NOT* from destructors)
            //but you can... the problem would be: where do you place the catcher? 
            //so better just display an error message and exit
            cerr<<"Uh-oh...file "<<file_name<<" not found"<<endl;
            exit(EXIT_FAILURE);
        

        //...read data...
        ginfo_file>>gravity_;
        //...
    

    double g_(void) const
    
        return gravity_;
    
private:
    double gravity_;
;

GlobalsFromFiles Gs("globals.dat");

int main(void)

    cout<<Gs.g_()<<endl;
    return 0;

【讨论】:

【参考方案10】:

全局不是邪恶的

必须先把它从我的胸口拿走:)

我会将常量粘贴到结构中,并创建一个全局实例:

struct Constants

   double g;
   // ...
;

extern Constants C =  ...  ;

double Grav(double m1, double m2, double r)  return C.g * m1 * m2 / (r*r); 

(简称也可以,所有科学家和工程师都这样做.....)

我使用了这样一个事实,即局部变量(即成员、参数、函数局部变量……)在某些情况下优先于全局变量,作为“穷人的一面”:

您可以轻松地将方法更改为

double Grav(double m1, double m2, double r, Constants const & C = ::C) 
 return C.g * m1 * m2 / (r*r);   // same code! 

你可以创建一个

struct AlternateUniverse

    Constants C; 

    AlternateUniverse()
    
       PostulateWildly(C);   // initialize C to better values
       double Grav(double m1, double m2, double r)  /* same code! */  
    

这个想法是在默认情况下以最少的开销编写代码,并保留实现,即使通用常量应该改变。


调用范围与源范围

或者,如果您/您的开发人员更喜欢程序化而不是 OO 风格,则可以使用 call scope 而不是 source scope,并使用全局值堆栈,大致:

std::deque<Constants> g_constants;

void InAnAlternateUniverse()

   PostulateWildly(C);    // 
   g_constants.push_front(C);
   CalculateCoreTemp();
   g_constants.pop_front();
 


void CalculateCoreTemp()

  Constants const & C= g_constants.front();
  // ...

调用树中的所有内容都可以使用“最新”常量。 OYu 可以使用一组备用常量调用相同的 tree 程序 - 无论嵌套多深。当然它应该被更好地封装,使异常安全,并且对于多线程,您需要线程本地存储(因此每个线程都有自己的“堆栈”)


计算与用户界面

我们以不同的方式处理您最初的问题:所有内部表示、所有持久数据都使用 SI 基本单位。转换发生在输入和输出处(例如,即使典型尺寸是毫米,它始终存储为米)。

我无法真正比​​较,但对我们来说效果很好。


维度分析

其他回复至少暗示了维度分析,例如各自的Boost Library。它可以强制维度正确性,并且可以自动进行输入/输出转换。

【讨论】:

以上是关于在 C++ 中创建全局“常量”的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

c++ 全局对象

在 PHP 中创建 DAO 的正确方法

在 C# 中创建 RTF 文件的正确方法是啥?

在 Java 中创建日期的正确方法是啥? [复制]

在 Twitter Bootstrap 中创建圆角的正确方法

在 kotlin 中创建泛型类的新实例的正确方法是啥?