【问题标题】:Are there any downsides to marking all variables you don't modify const?标记所有不修改 const 的变量有什么缺点吗?
【发布时间】:2016-12-19 21:22:05
【问题描述】:

经过多次谷歌搜索,我发现很多关于将函数及其参数标记为const,但没有关于将变量标记为const 的指南。

这是一个非常简单的例子:

#include <string>
#include <iostream>

void example(const std::string& x) {
  size_t length = x.length();
  for (size_t i = 0; i < length; ++i) {
    std::cout << x.at(i) << std::endl;
  }
}

int main() {
  example("hello");
}

为什么不做

size_t length = x.length();

类似常量

const size_t length = x.length();

按照惯例?

我知道这样一个小而简单的示例实际上并没有显示出任何巨大的好处,但它似乎在更大的代码库中会有所帮助,在这种情况下你可能会意外地改变一个你不应该改变的变量。

尽管有这样的好处,但我并没有真正看到它被使用得那么多(在我见过的 C++ 代码库中)或提到的几乎与制作函数及其参数 const 一样多。

除了必须输入 5 个额外的字符之外,这样做还有其他缺点吗?我在这个话题上没有找到太多的东西,如果有这么多的 const 是个问题,我不想自责。

【问题讨论】:

  • 这是一个有趣的问题,但我担心这个网站可能过于主观。 (就我个人而言,我说去吧!)
  • 我倾向于这样编写代码,但请记住,这可能会成为堆栈密集型(即,您不会重用临时对象)。
  • 将不改变的变量设为常量是提高代码可读性的好习惯。然而,人们很快就会变得懒惰。
  • "const 是你的朋友:不可变的值更容易理解、跟踪和推理,因此在合理的情况下更喜欢常量而不是变量,并且在定义值时将 const 作为默认选择 " -- "C++ 编码标准",Herb Sutter 和 Andrei Alexandrescu。
  • “const 在 C++ 中起作用的原因是因为你可以抛弃它。如果你不能抛弃它,那么你的世界就会糟透了。” - 安德斯·海尔斯伯格

标签: c++ refactoring constants


【解决方案1】:

标记不修改的变量const 没有缺点。

不过也有一些好处:当您无意中修改了您不应该/无意修改的变量时,编译器会帮助您进行诊断,并且编译器可能(尽管由于语言的原因const_castmutable 这很少见)生成更好的代码。

所以,我建议;尽可能使用const。没有缺点,您的编译器可能会帮助您发现错误。没有理由不这样做(除了一些额外的输入)。

请注意,这也扩展到成员函数。尽可能将它们设为const - 它可以让它们在更多上下文中使用并帮助用户推理代码(“调用此函数不会修改对象”是有价值的信息)。

【讨论】:

  • 过于冗长是一个缺点。它肯定会降低代码的可读性和难以维护。例如,在 OP 的代码段中,使 length const 不会增加真正的价值。是的,它将保护变量免受意外修改。另一方面,如果它被修改,代码可能比意外错字有更深层次的问题。这与作为 API 一部分的 x 形成对比,使其 const 是该功能要求其用户遵守合同的必要且重要的部分。
  • @SomeWittyUsername 永远不要低估一些初级维护程序员将来重构您的代码的能力,并且不注意某些变量不是打算修改的事实,从而意外引入错误 - 使用 @987654326 @ 尽你所能帮助阻止那些引入错误的人。我在现实生活中看到它发生的次数比我希望的要多..
  • 是的。但是这样的错误非常罕见,应该很容易被发现。另一方面,“consting”的影响是对整个代码库和所有从事它的人的整体影响。强调 - 我主要指的是一些琐碎的案例,比如这里的一段代码
  • 我不认为const_cast 对优化有太大的阻碍:如果 value 已声明为const,则尝试将其丢弃是UB。
  • 关于“没有缺点”的主题,如果变量具有类类型,并且在将来的某个时刻您决定从中std::moveconst 将导致复制构造函数如果这些构造函数以通常的方式声明 - 分别采用 const A&amp;A&amp;&amp;,则静默调用而不是移动构造函数。
【解决方案2】:

而不是这个¹非标准代码:

#import <string>
#import <iostream>

void example(const std::string& x) {
  size_t length = x.length();
  for (size_t i = 0; i < length; ++i) {
    std::cout << x.at(i) << std::endl;
  }
}

int main() {
  example("hello");
}

…我会这样写:

#include <string>
#include <iostream>
using namespace std;

void example( string const& s )
{
    for( char const ch : s )
    {
        cout << ch << '\n';
    }
}

auto main()
    -> int
{ example( "hello" ); }

相对于原始代码,我可以添加const 的主要位置是循环中的ch 变量。我觉得这很好。 const 通常是可取的,因为它减少了必须考虑的可能代码操作,并且基于范围的循环让您拥有更多 const

在大多数情况下使用const 的主要缺点是您必须与 C API 相关。

然后,人们只需要凭直觉就是否复制数据或信任文档并使用const_cast做出一些直觉决定。


        附录 1:
请注意,返回类型上的 const 会阻止移动语义。据我所知,Andrei Alexandrescu 在 Dobbs 博士杂志的 Mojo (C++03 move semantics) article 中首次提到了这一点:

[A] consttemporary 看起来像一个矛盾的说法,一个矛盾的术语。从实际的角度来看,const 临时人员在目的地强制复制。

所以,这是一个不应该使用const的地方。

对不起,我最初忘记提及这一点; user bogdan's comment 在另一个答案上提醒了我。


        附录 2:
同样(为了支持移动语义),如果对形式参数所做的最后一件事是将副本存储在某处,那么与其通过引用传递const,不如使用非const 参数按值传递,因为它可以简单地从中移动。

即,而不是

string stored_value;

void foo( string const& s )
{
    some_action( s );
    stored_value = s;
}

…或者优化的冗余

string stored_value;

void foo( string const& s )
{
    some_action( s );
    stored_value = s;
}

void foo( string&& s )
{
    some_action( s );
    stored_value = move( s );
}

……考虑写一下

string stored_value;

void foo( string s )
{
    some_action( s );
    stored_value = move( s );
}

对于左值实际参数的情况,它的效率可能会稍低一些,它放弃了const 的优点(对代码可能做的事情的限制),并且它打破了尽可能使用const 的统一约定,但它在任何情况下都不会表现不佳(这是避免这种情况的主要目标),而且它的代码更小,可能更清晰。


注意事项
¹ 标准 C++ 没有 #import 指令。此外,如果正确包含这些标头,则不能保证在全局命名空间中定义 size_t

【讨论】:

  • 一般情况下,我会将 range-for-loop 中的变量设为 const &amp;。对于某些类型,这无关紧要,对于某些类型,它避免了昂贵的副本。所以除非你明确地想要/需要一个笨蛋,const &amp; 似乎是理智的默认值..
  • @JesperJuhl 但在这种情况下,const&amp; 可能会减慢循环速度。复制单个 char 可能比取消引用要快。
  • 没有人会指出int main() 上的尾随返回类型(按照惯例应该是什么)或在“首选”方法中使用using namespace std 的无意义吗?
  • 我没有断言好的约定是没有意义的,我断言你的不遵循一个好的约定是没有意义的(充其量是适得其反)。 UNS 的论点是一匹死马。我会把我们的分歧留在原地。 “首选”是为了简化我可以称之为“您编写的代码”的消歧。
  • "auto main() -> int" - 真的吗?
【解决方案3】:

我知道这样一个小而简单的例子真的没有什么大不了的 受益于此,但似乎在更大的范围内会有所帮助 代码库,您可能会在其中意外地改变一个您不应该改变的变量 变异了。

问题是这基本上从未真正发生过。

另一方面,const 是一种疾病,像瘟疫一样在你的代码库中传播。一旦你声明了一个const 变量,你需要的所有东西都必须是const,所以它们只能调用const 函数,而且它永远不会停止。

const 在绝大多数情况下都不值得你为它付出的代价。只有少数情况下const 真正保护了您(例如设置密钥),但即便如此,如果您必须是一个彻头彻尾的白痴才能尝试这样做还是值得商榷的,而且可能不值得所有语言规则和不断的代码重复和冗余的元逻辑。

const 是一个不错的想法,理论上可能不错,但实际情况是const 完全是在浪费时间和空间。用火把它烧掉。

【讨论】:

  • 这就是我要找的哈哈。你有任何亲身经历过的例子吗?
  • 只要用Rust,就不用写const了。 ;-)
  • 我想在unordered_map 中使用指针作为键,然后在其上调用一些函数,这些函数都是非变异的,但后来我不得不将它们全部标记为 const,并且.. .
  • but the practical realities are that const is a total waste of time and space. Burn it with fire. 嗯,至少根据我的经验,到目前为止,由于意外的修改,我遇到的错误比由于膨胀的 const 使用而遇到的问题要多。是的,至少在接口级别,const 是一个设计元素,应该像每个设计元素和方面一样使用:有道理。
【解决方案4】:

我能想到至少两个缺点:

  • 冗长:更多的词,更多要处理的符号,...
  • 惯性:如果你需要修改它,你必须去删除这个const

两者都值得。


冗长是一个经常听到的反对明确性的论点,但是人们经常将阅读速度误认为理解速度。在冗长和明确之间找到一个平衡点,当然,太冗长可能会淹没有用的信息,但太隐含/简洁可能不会呈现必须重构/推断/推导/的信息。

就个人而言,我使用强类型静态检查语言,以便编译器尽早找出我的错误;用const 注释既向读者编译器提供信息。我认为这值得额外的 6 个符号。

至于惰性,删除 const 可能只是更改的一小部分成本......它通过迫使您遍历所有使用它的地方并检查周围的代码以确保它实际上没问题来回报自己删除此const。突然修改以前不可变的代码路径中的特定数据需要确保代码路径的任何部分(或其调用者)都不会意外依赖这种不可变性。

【讨论】:

  • 随着代码的变化,如果你不需要const,那么稍后删除它要比添加它要容易得多,如果你真的应该这样做清楚的常量。在我看来,简单声明上的const 是为它传达给读者的信息支付的一小笔费用。当const 存在时,我知道创建临时变量只是为了给表达式赋予一个有意义的名称,而不是因为它是一个会随着时间而改变的属性,并且可能必须遵守一些不明显的不变量。
  • @AdrianMcCarthy:完全同意。
  • 我认为惯性论点很重要。虽然每个人都说“删除const 很容易”,而且他们是对的,但您确实冒着使其他解决方案更容易执行的风险,例如将值复制到新变量中。如果截止日期很好,我不会这样做,但是在发布前的最后 2 小时,当代码仍然无法正常工作时,这样的小技巧非常容易制作。事实上,就在今天,我不得不对一个论点(不是本地的)这样做。接口中的参数是 const ,但它不是必须的。没时间修界面,赶紧复制吧!
  • @CortAmmon:如果你能负担得起复制的性能,我认为复制更好,因为对本地的任何更改都是......本地的。无需担心广泛的影响或任何事情。从维护的角度来看,这种保证是无副作用的 wrt。这个论点是一个很好的属性。另外,特别是关于你的轶事,如果它没有被注释 const 并且你已经修改了它......你有多大信心你没有不小心破坏了一些东西?如果您没有时间检查它用于保证他们不依赖于不可变的参数......
  • @MatthieuM。诚然,如果所有代码都写得很完美,我们只会写完美的代码 =) 但是问题是现在的代码仍然有惯性,而且不如以前那么完美。现在,第二次调整可能会产生更多不需要的复杂性。如果你有无限的重构预算,这没关系。但是,当您在重构与其他预算压力之间取得平衡时,很容易陷入没有足够的精力来修复代码的情况,因此我们反而让情况变得更糟。我讨厌产生惯性的代码,因为它有惯性。
【解决方案5】:

对于像这样的简短方法中的局部变量size_t length,这并不重要。额外冗长的缺点基本上与避免错别字意外修改长度的相对安全性相平衡。随你当地的风格指南或你自己的直觉告诉你什么。

对于更长或更复杂的方法,可能会有所不同。但是话又说回来,如果您有一个重要的方法,也许您至少应该考虑将代码重构为更简单的部分......无论如何,如果您阅读并理解代码,显式const 提供的额外提示有点无关 - 很好但无关紧要。


有点相关,虽然你没有问过它:对于你的example 方法的引用参数,你肯定想要const,因为你可能需要给它传递一个常量字符串。只有当你想禁用传递 const 字符串时(因为你认为你会添加代码来修改它),你应该在那里省略 const

【讨论】:

    【解决方案6】:

    我同意到目前为止给出的大多数答案,另一方面,某些方面仍然缺失。

    定义接口时,const 关键字是您的朋友。但你应该知道,它也有些局限,有时甚至是自私的——这就是我所说的它的缺点。

    让我们再仔细看看这个问题:

    将所有不修改的变量标记为 const 有什么缺点吗?`

    如果您观察到您没有修改某些东西,您可能会说它实际上是一个常数。代码分析工具也可以检测到这一点,即使你的编译器已经知道它。但是这个观察不应该触发你的add-const reflex

    想想变量本身,问问

    • 有用吗?
    • 它是否也打算保持不变?

    有时可以简单地删除一个中间变量,有时可以做一个小的返工(添加或删除一个函数)来改进代码。

    添加 const 关键字可能会强化您的代码,以防止其他地方出现错误,但也可以防止在声明点发生更改。

    我还要补充一点关于不变的成员变量。如果决定声明成员变量const,则必须在包含类的构造函数的初始化列表中对其进行初始化,这扩展了构造该类对象的前提条件。

    所以不要在编译器允许的任何地方添加const。不要被引诱“石化你的代码”;-)

    【讨论】:

      猜你喜欢
      • 2011-08-07
      • 2011-04-26
      • 2011-02-28
      • 1970-01-01
      • 2013-07-26
      • 1970-01-01
      • 2021-05-08
      • 1970-01-01
      • 2011-11-05
      相关资源
      最近更新 更多