【问题标题】:C++ const keyword - use liberally?C++ const 关键字 - 随意使用?
【发布时间】:2010-12-06 00:00:33
【问题描述】:

在以下 C++ 函数中:

void MyFunction(int age, House &purchased_house)
{
    ...
}


void MyFunction(const int age, House &purchased_house)
{
    ...
}

哪个更好?

在两者中,“年龄”都是按值传递的。我想知道 'const' 关键字是否必要:这对我来说似乎是多余的,但也很有帮助(作为变量不会改变的额外指示)。

有没有人认为以上哪一项(如果有的话)更好?

【问题讨论】:

标签: c++ syntax constants


【解决方案1】:

首先,它只是一个实现细节,if 你把 const 放在那里,不要把它放在声明集(标题)中。只放在实现文件中:

// header
void MyFunction(int age, House &purchased_house);

// .cpp file
void MyFunction(const int age, House &purchased_house);
{
    ...
}

定义中的参数是否为 const 纯粹是实现细节,不应成为接口的一部分。

我不经常看到这种事情,我也不会这样做。拥有参数 const 会比帮助更让我感到困惑,因为我会立即将其模式匹配失败为“const int &age” :) 当然,这与在另一个级别拥有 const 完全不同:

// const is a *good* thing here, and should not be removed,
// and it is part of the interface
void MyFunction(const string &name, House &purchased_house);
{
    ...
}

在这种情况下,const 会影响函数是否可以改变调用者的参数。应该尽可能多地使用这个含义的 const,因为它可以帮助确保程序的正确性并改进代码的自文档化。

【讨论】:

  • @Brian,我没有这么说。问题是关于何时按值传递参数。在这种情况下,当您将 const 放在顶层并因此使参数本身为 const 时,这是一个实现细节。
  • @San Jacinto - 调用者已经知道它不会在从函数返回时被修改,因为它不是从函数返回的:它是按值传递的,所以你传入一个副本并且一无所获。至于被调用者是否修改他的本地副本,这是一个实现细节(在CPP文件中而不是在header中)。
  • @gab:但是你不能在一个参数过去的常量上重载它。因此 litb 提出了很好的建议。
  • 我了解技术差异。我的意思是,作为一个使用别人代码的程序员,我想要保证我的 foo 会是一样的回来,或者我想知道它可以在其他人的代码中被改变。另外,我不想看。然而,我想有很多话要说,因为稍后其他人可以在不让我知道的情况下将其改回,现在我同样被搞砸了。
  • 桑,你确定你没有在参考文献中尝试这个吗? int foo(int &)int foo(const int &) 确实不一样。
【解决方案2】:

我推荐阅读Herb Sutter. Exceptional C++。有一章“Const-Correctness”。

“事实上,对于编译器来说,无论你是否在值参数前包含这个const,函数签名都是一样的。”

表示这个签名

void print(int number);

实际上与此相同:

void print(int const number);

因此,对于编译器而言,您声明函数的方式没有区别。而且你不能通过将 const 关键字放在传值参数前面来重载它。

进一步了解 Herb Sutter 的建议:

"避免在函数声明中使用 const 传值参数。如果不修改,仍将参数 const 放在同一个函数的定义中。"

他建议避免

void print(int const number);

因为 const 令人困惑、冗长且多余。

但在定义中,您应该这样做(如果您不更改参数):

void print(int const number)
{
   // I don't want to change the number accidentally here
   ...
}

因此,您可以确定,即使在 1000 行函数体之后,您始终可以保持数字不变。编译器将禁止您将数字作为非常量引用传递给另一个函数。

【讨论】:

  • 我认为你提到的 Exceptional C++ 的 Const-Correctness 是其中之一:gotw.ca/gotw/006.htm
  • 是的,整本书都是基于gotw的。但是这本书解释得更好,IMO。
【解决方案3】:

恕我直言,这是过度使用。 当您说“const int age,...”时,您实际上说的是“您甚至无法更改函数内部的本地副本”。您所做的实际上是通过在他想要更改年龄/通过非常量引用传递它时强制他使用 another 本地副本来降低程序员代码的可读性。

任何程序员都应该熟悉引用传递和值传递之间的区别,就像任何程序员都应该理解“const”一样。

【讨论】:

  • 强制程序员使用另一个局部变量名而不是重用正式的输入参数是一件好事。通过为其分配一个新值来将参数重用于新目的,通过赋予该名称多个目的来降低代码的可读性。某些语言不允许这样做,例如 C#。
  • -1。使用 const 实际上有助于在这里进行调试。通过强制程序员使用 LOCAL 变量,它可以帮助她(和她的读者)真正理解更改不会传播到方法/函数之外。
  • 这一切都归结为一个问题“读者在编程方面的水平如何,他们习惯了什么?”对于初学者来说,异常、引用语义甚至隐式转换都可能看起来不清楚和不可读。当我编写代码时,我会尽量保持清晰。然而,我并不打算让每个初学者都能理解。对我来说更重要的是避免经验丰富的程序员可能错误地做的模式匹配(由 litb 和 idimba 在他们的回答中提到)(就像我可能会做的那样)。我希望读者能够理解逐个复制。
  • 说实话,我也不明白目前这个低点是怎么回事。他提出了一个很好的观点,它只是在说一些关于本地事情对呼叫者没有影响的事情。他关于 force-creation-of-new-local 的观点可能有点离题(顺便说一句,cpp-next.com/archive/2009/08/want-speed-pass-by-value 鼓励您直接修改参数),但我认为不值得-5。正如其他人已经指出的那样,无论是否为 const,它都不会将更改传播到按值传递的参数,因此上面的“-1”注释也不太正确。
  • 哇,这个答案的反对票数是惊人的。对于那些投反对票的人:通过 const 值获取参数没有任何优势,实际上,如果参数是临时的,则通过 const 引用获取参数并稍后复制它可以阻止虚假复制。 Matthieu M 和 Partial 的 cmets 看起来很业余。我们这些有经验的人会注意到参数是通过复制还是通过引用,而我并不是为了让新手可以阅读而编写更糟糕的代码。
【解决方案4】:

你不仅表明它不会改变,它还会阻止你的代码认为它可以,它会在编译时被捕获。

【讨论】:

    【解决方案5】:

    请参阅 Meyers 和“Effective C++”,并大量使用 const,尤其是通过引用传递语义。

    在这种原子变量的情况下,效率并不重要,但代码清晰仍然有好处。

    【讨论】:

    • 对于按值传递的原始类型,我不同意。如果我在代码中看到这一点,我会做双重考虑,因为被调用者无法更改值(从调用者的角度来看)。我想知道是否有一些错字,如果编写代码的人只是不理解按值传递。
    • @Dolphin:那么您应该阅读 litb(一如既往)对该主题的出色处理,学习并停下来思考。如果代码感觉“自然”还是“习惯”是正确性的标准,那么没有人会开始将 STL 用于所有这些乍看之下感觉如此不自然的有趣迭代器。
    • 如果他们避免了这种情况,也许我们现在已经对 C++ 中的范围提供了不错的支持;-p
    • @onebyone:实际上,我支持你。 :) 我不确定删除概念的影响,但 C++1x 将提供 IMO 不错的范围支持。
    • @sbi,嗯,我和海豚在一起,他似乎同意我在回答中所说的话。
    【解决方案6】:

    好吧,正如其他人已经说过的那样,从C++语言的角度来看,上面的const对函数签名没有影响,即无论const是否存在,函数类型都保持不变.它在抽象 C++ 语言级别的唯一作用是不能在函数体内修改此参数。

    但是,在较低级别,应用于参数值的const 修饰符可以具有一些优化优势,前提是编译器足够聪明。考虑两个具有相同参数集的函数(为简单起见)

    int foo(const int a, const double b, const long c) {
       /* whatever */
    }
    
    void bar(const int a, const double b, const long c) {
       /* whatever */
    }
    

    假设在代码中的某处它们被称为如下

    foo(x, d, m);
    bar(x, d, m);
    

    通常,编译器在调用函数之前准备带有参数的堆栈帧。在这种情况下,堆栈通常会准备两次:每次调用一次。但是聪明的编译器可能会意识到,由于这些函数不会更改它们的本地参数值(用const 声明),为第一次调用准备的参数集可以安全地用于第二次调用。因此它只能准备一次堆栈。

    这是一种相当少见且晦涩难懂的优化,它只能在函数的定义在调用时已知(相同的翻译单元或高级全局优化编译器)时起作用,但有时它可能是值得一提。

    说它“毫无价值”或“没有效果”是不正确的,即使使用典型的编译器可能就是这种情况。

    另一个值得一提的考虑是不同的性质。那里有编码标准,要求编码人员不要更改初始参数值,例如“不要将参数用作普通局部变量,参数值应在整个函数中保持不变”。这是有道理的,因为有时它可以更容易地确定函数最初给出的参数值(在调试器中,在函数体内部)。为了帮助执行此编码标准,人们可能会在参数值上使用const 说明符。值不值得是另一个问题...

    【讨论】:

    • 恐怕这种优化只有在编译器能够发现在对foo的调用中没有任何改变xd和@987654331 @ - 这比你想象的要难。
    • 不,你没有抓住重点。一旦将参数声明为“const”(如我的示例中所示),编译器就不必“找出”任何东西。它可以安全地假设在 'foo' 中没有任何东西可以合法地改变参数的值,这正是在那里使用 'const' 的关键。如果你设法“破解”'const' 给编译器的承诺,那是你自己的错,编译器不负责捕捉它。行为未定义,所有赌注均已取消。这正是 'const' 的工作原理,并且始终在 C/C++ 中工作。
    • 我认为 sbi 考虑到了别名。就编译器所知, foo 的实现可以访问指向 x 的指针(可能它存储在全局中,或者通过其他一些参数到达)。即使 a (foo 的 x 的副本) 是 const,foo 也可能会写入它,但会产生修改 x 的副作用。那么必须传递给 bar 的值是 x 的新值,而不是传递给 foo 的旧值。除非编译器可以排除这一点,否则它不能将 x 的潜在“陈旧”副本作为参数传递给 bar。所以这比你想象的要难,因为混叠通常很难排除。
    • .. 基本上,如果 foo 通过指针或引用写入任何 int、unsigned int 或 char 值,或者调用任何执行此操作或无法分析的代码,那么优化不可能。即使启用了严格的别名规则 - 如果没有它们,如果 foo 通过任何指针或引用写入任何内容,那么你就完成了。在这两种情况下,DFA 理论上可以通过证明实际上指针不是别名来节省时间,如果编译器可以弄清楚它确实指向什么,或者(更有可能)弄清楚 x、d 和 m 从来没有他们的地址被占用了。
    • @onebyone:谢谢,你成功了。 (虽然我不知道“DFA”代表什么。)本质上:这是可能的,但比大多数人想象的要难。
    【解决方案7】:

    使用const 的一个好处是您不会意外更改age 中间的age 的值(如果您忘记它不是通过引用传递的)。一个“缺点”是您无法使用foo.process(++age); 之类的代码回收age

    【讨论】:

    • foo.process(++age) 应该没问题,因为 const 适用于 process 可以对参数执行的操作 - int age 是可变的...它转换为 const int 在函数内访问时。
    • @D.Shawley:我说的是foo.process(++age);,而不是MyFunction(++age, bar);
    【解决方案8】:

    你是对的,“const int age”的唯一目的是不能改变年龄。然而,对于大多数程序员来说,这可能会非常令人困惑。因此,如果这些方法没有在您的代码中广泛使用,我建议省略 const。

    【讨论】:

    • ""const int age" 的唯一目的是不能改变年龄" 你这么说好像是一件小事。 IMO 让编译器检查 const 正确性的能力是 C++ 的最大优势之一。
    • 我完全赞成编写可理解的代码,但不是为了换取编译时检查的失败。也就是说,如果这种方法“没有在您的代码中广泛使用”,我会说您的团队需要一些教育和/或更好的代码审查,以便它变得被广泛使用。
    • 似乎默认情况下传值应该是const,不是吗?然而,这并不是你在实际代码中经常看到的结构——事实上我认为我从未见过它。
    • @litb:这就是你错的地方。这不是混淆代码。这是安全代码。也许你不习惯让你的代码安全,但额外的努力充其量可以让编译器优化你的代码,最坏的情况就是保护!
    • @idimba:相反,它实际上优化了代码,因为在汇编程序中你将只有一个变量保持不变,而不是有很多变量。在一个大项目上,如果你使用关键字'const'会更有价值。同时,使用“const”,您可以在编译代码时进行免费检查,以验证常量是否保持不变。阻止编译器优化代码的是关键字 'volatile',它在执行多线程应用程序时非常有用!
    【解决方案9】:

    第二个变种更好。在第一个中,您可能会意外更改变量年龄。

    【讨论】:

    • 也许您需要更改它而不创建另一个变量。
    • @Justicle:为什么?如果您定义一个新变量并对其进行修改,您的程序将更加清晰。编译器应该优化它,所以你得到相同的编译程序集,但它更容易阅读和维护(例如,你以后可能会发现你在函数后期需要原始参数值)。
    【解决方案10】:

    当(且仅当)您将在任何其他不会被修改的局部变量上使用它时,您应该在参数上使用const

    const int age_last_year = age - YEAR;
    

    在可能的情况下标记局部变量 const 有时很方便,因为这意味着您可以查看声明并知道这是值,而无需考虑中间代码。您可以在将来轻松更改它(并确保您没有破坏函数中的代码,并且如果它现在代表稍微不同的东西,可能会更改变量的名称,这是可变的,而不是之前的不变东西)。

    与此相反,它确实使代码更加冗长,而且在一个简短的函数中,几乎总是非常明显哪些变量发生了变化,哪些变量没有发生变化。

    所以,要么:

    void MyFunction(const int age, House &purchased_house)
    {
        const int age_last_year = age - YEAR;
    }
    

    或:

    void MyFunction(int age, House &purchased_house)
    {
        int age_last_year = age - YEAR;
    }
    

    【讨论】:

      【解决方案11】:

      我找到了一些很棒的文章和一本书来解释使用 const 的优点:


      我可以向您提出以下格言吗?

      如果一个对象/变量可以被限定为常量,它应该是。在最坏的情况下,它不会花费任何费用。充其量,您将记录对象/变量在代码中的作用,这将使编译器有机会进一步优化您的代码。

      一些编译器忽略了使用“const”来挖掘优化的潜力,这导致许多专家在可以使用常量参数时忽略了按值使用常量参数。这种做法需要更严格,但不会有害。在最坏的情况下,您不会失去任何东西,但您也不会获得任何东西,而在最好的情况下,您会从使用这种方法中获利。


      对于那些似乎不理解 const 在函数/方法的按值参数中的效用的人...这里有一个简短的示例来解释原因:

      .cpp

      void WriteSequence(int const *, const int);
      
      int main()
      {
          int Array[] = { 2, 3, 4, 10, 12 };
          WriteSequence(Array, 5);
      }
      
      
      #include <iostream>
      using std::cout;
      using std::endl;
      void WriteSequence(int const *Array, const int MAX)
      {
          for (int const * i = Array; i != Array + MAX; ++i)
            cout << *i << endl;
      }
      

      如果我去掉 int MAX 前面的 const 并在里面这样写 MAX + 1 会发生什么?

      void WriteSequence(int Array[], int MAX)
      {
          MAX += MAX;
          for (int * i = Array; i != Array + MAX; ++i)
            cout << *i << endl;
      }
      

      那么你的程序会崩溃!现在,为什么有人会写“MAX += MAX;” ?也许是人为错误,也许程序员那天感觉不舒服,或者程序员根本不知道如何编写 C/C++ 代码。如果你有 const,代码甚至都不会编译!

      安全代码是好代码,当你拥有它时添加“const”不需要任何成本!


      这是一个非常相似的问题的不同帖子的答案:

      "const 是没有意义的,当参数 是按值传递的,因为你不会 正在修改调用者的对象。”

      错了。

      这是关于自我记录您的代码 和你的假设。

      如果您的代码有很多人在工作 在它上面,你的功能是 非平凡的那么你应该标记 "const" 你的任何东西 能够。写工业实力的时候 代码,你应该总是假设 你的同事是精神病患者 尽他们所能得到你 (尤其是因为通常是你自己 将来)。

      另外,正如有人提到的 早些时候,它可能会帮助编译器 稍微优化一下(虽然它是一个 远射)。

      这里是链接:answer

      【讨论】:

      • 好吧,我/我们同意为值参数编写 const 是不是毫无意义的。它还可以增加安全性。当人们可以使用 java 并且“更安全”时,为什么还要编写原始 C 代码?因为 C 速度很快,而 C 是低级的。安全不是一切——还有其他问题,例如可读性、清晰度。您在这个答案中的主要观点是“const”很有用——我想我们都同意。但是坚持“const” anywhere - 这就是我们不同意的地方。我不太明白你的前两个链接 - 他们一般谈论“const”,而不是关于顶级值参数的“const”
      • 我认为这个讨论永远不会结束。你有你的意见,有很好的反映背景,我有我的意见,也有很好反映的背景。我怀疑我们俩都不会“转变”为对方的观点,因此我怀疑参与此讨论的大多数其他人。
      • @litb:对于第一个链接,请查看结论,对于第二个链接,它说尽可能放置一个 const。这或多或少是我使用 const 的标准,它将完成我的代码更清洁。
      • @litb:您能否详细说明为什么在按值传递的参数中为数据类型编写 const 是不可读的?如果我没记错的话,通过引用传递原始数据类型(例如整数)不会获得任何性能或几乎没有任何性能,因为它将隐式地在整数上创建一个指针,以便您能够使用声明的变量或常量在函数/方法之外。因此,使用传值常量不会产生任何问题。
      • 你为什么不听从你的例子中的建议?如果你这样做了,那就是void WriteSequence(int const* const Array, const int MAX)
      【解决方案12】:

      拥有一个通过值 const 传递的原始类型几乎毫无价值。将 const 传递给函数通常用作与调用者的合同,即函数不会更改值。在这种情况下,由于 int 是按值传递的,因此函数无法进行任何在函数外可见的更改。

      另一方面,如果不对对象进行任何更改,rreferences 和非平凡对象类型应始终使用 const。从理论上讲,这可能会进行一些优化,但最大的胜利是我上面提到的合同。缺点当然是,它可以让你的界面变得更大,并且 const 很难改造到现有系统中(或者使用不使用 const 的第 3 方 API)。

      【讨论】:

        猜你喜欢
        • 2022-01-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多