尽可能保持“常量”是一个好主意吗?

时间:2011-12-16 09:23:59

标签: c++ coding-style const

最近,我一直在开发一种在我的代码中用const创建许多东西的做法:

(1)功能参数,我知道永远不会改变。 e.g:

void foo (const int i, const string s)
          ^^^^^        ^^^^^ 

(2)返回类型const。 e.g:

struct A {
...
  const int foo () { return ...; }
  ^^^^^
  operator const bool () const { return ...; }
           ^^^^^
};

(3)整数或字符串的平凡计算。 e.g:

const uint size = vec.size();
^^^^^
const string s2 = s1 + "hello ";
^^^^^

......还有更多的地方。通常在其他现实世界的代码中,我没有看到标记为const的小规模变量。但我想,让它们const永远不会受到伤害。这是一个很好的编程实践吗?

7 个答案:

答案 0 :(得分:13)

(1)和(3)密切相关。 by-value参数只是具有该名称的局部变量,这是计算的结果。

通常,无论是否标记局部变量const,短函数都没有什么区别,因为您可以看到它们的整个范围就在您面前。您可以查看值是否更改,您不需要或希望编译器强制执行它。

但是,它偶尔会有所帮助,因为它可以保护您不会意外地将它们传递给通过非const引用获取其参数的函数,而不会意识到您正在修改变量。因此,如果您将变量作为函数参数在其生命周期中传递,那么将其标记为const可以让您更自信地知道它之后具有的值。

非常偶然,标记变量const可以帮助优化器,因为你告诉它对象永远不会被修改,有时这是真的,但编译器不能证明给我看。但由于这个原因,它可能不值得这样做,因为在大多数情况下它没有任何区别。

(2)是另一回事。对于内置类型,它没有区别,正如其他人所解释的那样。对于类类型,按const值返回。这似乎是一个好主意,因为它可以防止用户写出像func_returning_a_string() += " extra text";这样毫无意义的东西。但它也阻止了一些有意义的东西--C ++ 11移动语义。如果foo返回一个const字符串,并且我写std::string s = "foo"; if (condition) s = foo();,那么我会在s = foo();处获得副本分配。如果foo返回非const字符串,那么我将获得移动赋值。

类似地,在没有移动语义的C ++ 03中,它阻止了称为“交换优化”的技巧 - 使用非const返回值我可以写foo().swap(s);而不是s = foo();

答案 1 :(得分:7)

是。

用编程语言表达你的意图始终是一个很好的编程习惯。您不打算更改变量,因此请将其设为const。稍后,当你的编译器向你大吼大叫,当它是const时你无法修改它,你会很高兴编译器发现了你自己设计的一些错误想法。这不仅适用于const,也适用于许多其他事物,例如为什么在C ++ 11中引入了override关键字。

当然有些情况下const不会改变任何东西,例如当你返回int时,但就像在其他安全区域一样:更好的是有一个const太多(你可能会删除以后因为它不是“真的需要”,而不是让一个人太少(这会突然间突然破坏)。

答案 2 :(得分:3)

标量返回类型的

const被忽略,因为没有标量const rvalue这样的东西。

即,以下两个声明完全等效:

int foo();
const int foo();   // this particular const is ignored by the compiler

如果您考虑一下,这是有道理的:无论如何,如果没有foo() = 42;,您就无法写const

答案 3 :(得分:2)

通常,Const正确性很重要 - 但您使用的示例并非真正重要的地方。例如:

void foo (const int i, const string s)

我会非常大胆地声明这个原型错误。如果你刚才有原型

void foo (int i, string s)

然后已经承诺不修改用户为is传入的值。 (因为你获得了这些值的副本,而不是对原文的引用)所以,const在这种情况下唯一要做的就是请求编译器抱怨如果你不小心试图修改is。但是还有其他方法可以实现这种效果 - 它实际上没有任何业务暴露在外部。

现在,const的以下用法实际上很有用:

void foo (int i, const string &s)

这保留了您不会修改s的承诺,但现在您获得了性能优势,因为它不再涉及s的副本。

其他一些用途很好:

const size_t size = vec.size();

这很好。 (一旦用uint替换size_t)这里没有性能优势,但它是自我记录的,可以保护您免受愚蠢的事故。

const string s2 = s1 + "hello ";

这也很好。虽然我可能会再把它作为参考:

const string &s2 = s1 + "hello ";

这一个

operator const bool () const { return ...; }

是最无关紧要的。您应该实现operator bool而不是operator const bool,即使除了前者之外没有任何理由是人们所期望的。

但对于拥有非const版本的类来说,它可能很重要。返回const,那不是品味或风格的问题,而是一个声明“我将返回给你一个新的对象,但是你不允许调用它的任何非const函数” 。这通常是一个坏主意 - 如果你真的想要做出声明,只能使它成为const

答案 4 :(得分:1)

绝对是,至少对于需要长期保持可维护的代码。

如果您想了解更多信息,Scott Meyers的“Effective C ++”一书显示了需要const返回类型和方法参数的数字场景,以保护您的代码免遭滥用。

答案 5 :(得分:1)

让我们看一个例子:

class Foo {
private:
    int x;
public:
    Foo () : x (0) { }
    Foo (const Foo& other) { x = other.x; }
    void setX(const int newX) {
        x = newX;
    }
    const int getX() const {
        return x;
    }
    const Foo getFoo() const {
        return *this;
    }
};

让getX()返回const int vs int是愚蠢的。任何POD按值返回类型都是无意义的。为什么重要?你可以对非const POD变量做的唯一不能做的就是赋值...而且lvalue / rvalue的区别就是返回类型。

相比之下,当它是一个按值返回的对象时,const可以有所作为...因为可能有一些操作你可以在非const对象上执行,你不能在一个常数上。但是,这可能会被const复制构造函数所破坏。我们以getFoo()为例:

Foo foo;
/* foo.getFoo().setX(10); */ // ERROR!
// because f.getFoo() returns const Foo

Foo bar;
bar = foo.getFoo();
bar.setX(10); // no error

制作按值POD参数类型const可防止在函数内修改该参数。因此,例如,您无法在newX内为setX分配值。

所有事情都是平等的,这是正确的......因为采用int参数并在函数内部进行更改是一种令人困惑的做法;如果你正在调试并想知道调用的函数是什么,你必须上调调用栈才能找到答案。但是所有事情都不平等,因为它更多的打字和屏幕混乱。

类似地,将局部POD变量设为const更容易打字和混乱,以减少回报。我只在全局上使用它。

所以对我来说,我说const应该保留用于具有真正不同的语义方法分派的对象(通常由缺少const拷贝构造函数表示)...或全局变量。否则,这只是为了获得轻微或无益的利益。

答案 6 :(得分:1)

  

(1)功能论证,我知道永远不会改变。

通过值const传递参数的唯一原因是您可能希望避免意外更改函数内的副本值,如:

void f( int x ) {
  x += 2;
  // OOPS: At this point 'x' is actually not what the caller passed to us!
}

如果将参数作为const int x传递,则不会发生这种情况。但是,我知道很多代码,其中程序员故意将参数作为值而不是const传递,因为他希望修改参数 - 否则他会复制无论如何。即而不是

void f( const std::string &s ) {
    std::string s2 = s;
    s2[0] = 'A';
    // ...
}

他们做了

void f( std::string s ) {
    s[0] = 'A';
    // ...
}

故意修改这样的值副本的好方法是你不必考虑令人讨厌的命名问题(如果参数为s_并且已清理的值为{{1} }?或ss2,或者是什么?)。

由于这里s的存在或不存在根本不影响调用者(无论如何你都得到了值的副本,所以你不能修改调用者的数据),它只是归结为:某些事情是有道理的。有时却没有。这里没有好的指导方针。我个人的感觉是你不应该打扰。如果你想让参数const避免你意外 oevrwrite它,那么你的函数可能在一开始就太长了(或者参数名称是愚蠢的,比如{{1或者consti之类的东西。)

  

(2)返回类型为const。

只有在您认为代码的调用者可能意外尝试修改函数返回的值时才有意义,如下所示:

tmp

如果您要禁止it或其他变异通话,则可以QString f() { return "Hello"; } 类型f.strip()。我曾经知道一个很好的例子,将一个对象作为非const值返回实际上允许一些相当令人惊讶的客户端代码。不幸的是我不记得了。

  

(3)整数或字符串的平凡计算。

这实际上与我在上面讨论的(1)完全相同。同样的道理也适用。