关于c ++:尽可能保持“const-ness”是一个好主意吗?

Is it a good idea of maintaining “const-ness” as much as possible?

最近,我开发了一种实践,在我的代码中生成许多东西,比如const

(1)函数的参数,我知道永远不会改变。例如。:

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

(2)返回类型为const。例如。:

1
2
3
4
5
6
7
struct A {
...
  const int foo () { return ...; }
  ^^^^^
  operator const bool () const { return ...; }
           ^^^^^
};

(3)整数或字符串的简单计算。例如。:

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

…以及其他一些地方。通常在其他现实世界代码中,我看不到这样的小尺度变量标记为const。但我想,让他们去做不会伤害他们。这是一个好的编程实践吗?


(1)和(3)密切相关。按值参数只是具有该名称的局部变量,计算结果也是如此。

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

但是,有时它确实有帮助,因为它可以防止您意外地将它们传递给一个通过非常量引用获取其参数的函数,而不会意识到您在修改变量。因此,如果您在变量的生命周期中将其作为函数参数传递,那么将其标记为const,可以让您更加确信您知道它之后有什么值。

有时,标记一个变量const可以帮助优化器,因为您告诉它对象从未被修改过,有时这是正确的,但编译器无法证明它。但由于这个原因,可能不值得这么做,因为在大多数情况下,这没有什么区别。

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

同样在C++ 03中,它没有移动语义,它阻止了被称为"SimaPimTimes"的技巧——具有非const返回值,我可以编写EDCOX1,8,而不是EDCOX1×6。


对。

用编程语言表达您的意图始终是一种很好的编程实践。你不打算改变变量,所以把它设为常量。稍后,当编译器对您大喊大叫说当它是常量时您不能修改它时,您会很高兴编译器发现了您自己设计的一些错误想法。这不仅对const是正确的,而且对于许多其他事物来说也是如此,这就是为什么在C++ 11中引入了EDCOX1×0 }关键字。

当然,在某些情况下,const不会改变任何东西,比如当你返回一个int时,但就像在其他安全区域一样:与其拥有一个太少的const(它会突然无声地打断东西),不如拥有一个太多的const(因为它不是真正需要的,你可能会在以后移除)。


忽略标量返回类型上的consts,因为没有标量const rvalue之类的东西。

也就是说,以下两个声明完全相同:

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

如果你想一想,这是有道理的:无论如何,没有const,你不能写foo() = 42;


一般来说,常量正确性很重要——但是您使用的示例并不是它真正重要的地方。例如:

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

我敢说这个原型是错的。如果你有原型的话

1
void foo (int i, string s)

那么,这已经是一个承诺,不修改用户为is传递的值。(因为您得到了这些值的副本,而不是对原始值的引用),所以,在这种情况下,const所做的唯一一件事就是请求编译器在您无意中试图修改is时提出投诉。但还有其他方法可以达到这种效果——它实际上没有任何业务暴露在外部。

现在,使用EDOCX1[0]实际上是有用的:

1
void foo (int i, const string &s)

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

其他一些用途很好:

1
const size_t size = vec.size();

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

1
const string s2 = s1 +"hello";

这也不错。不过,我还是会把它作为参考:

1
const string &s2 = s1 +"hello";

这一个

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

是最无关紧要的。你可能应该执行operator bool,而不是operator const bool,即使除了前者之外没有其他原因也是人们所期望的。

但对于具有非常量版本的类来说,这可能很重要。返回const,这不是一个品味或风格的问题,而是一个声明:"我将向您返回一个新对象,但不允许您调用它的任何非常量函数。"这通常是一个坏主意——只有当你真的打算做那个声明的时候,才把它变成const


(1) Arguments to function, which I know never going to be changed.

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

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

如果你以const int x的身份传递论点,就不会发生这种情况。但是,我知道很多代码,程序员有意将参数作为一个值传递,但不传递常量,因为他想修改参数——否则他会复制一个参数。即代替

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

他们这样做

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

故意摆弄这种值的副本的好的副作用是,您不必考虑烦人的命名问题(参数应该是s_,净化后的值应该是s?或者说是s2s还是什么?.

由于这里的const的存在或不存在根本不会影响调用者(无论如何,您都会得到一个值的副本,因此您无法修补调用者的数据),它简单地归结为:一些它是有意义的。有时不会。这里没有好的指导方针。我个人的感觉是你不应该麻烦。如果你想让参数const避免你不小心写出来,那么你的函数可能在一开始就太长了(或者参数名很傻,比如i或者tmp或者it之类的)。

(2) Return types as const.

只有当您认为代码的调用方可能无意中尝试修改函数返回的值时,这才有意义,如下所示:

1
QString f() { return"Hello"; }

如果你想禁止f.strip()或其他变异调用,你可以使用returnconst。我曾经知道一个很好的例子,在这种情况下,将对象作为非常量值返回实际上允许一些相当令人惊讶的客户机代码。不幸的是,我再也记不起来了。

(3) Trivial computation of integer or strings.

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


让我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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与一个int是愚蠢的。按值类型返回的任何pod都是无意义的。为什么重要?对于非const pod变量,唯一不能对const变量执行的操作是赋值……lvalue/rvalue的区别处理返回类型的问题。

相反,当它是一个按值返回的对象时,const可以起到作用…因为可能有一些操作可以在非const对象上执行,而不能在const对象上执行。不过,这可能会被一个const-copy构造函数破坏。以getfoo()为例:

1
2
3
4
5
6
7
Foo foo;
/* foo.getFoo().setX(10); */ // ERROR!
// because f.getFoo() returns const Foo

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

使by-value pod参数类型const可以防止在函数内修改该参数。例如,您不能在setX的内部为newX赋值。

所有的东西都是相等的,那就好了……因为在函数内部取一个int参数并更改它是一种令人困惑的做法;如果您正在调试并且想要知道函数是用什么调用的,那么您必须向调用堆栈上查找。但所有的东西都不一样,因为它更容易打字和屏幕混乱。

同样地,使局部POD变量变为常量会增加类型和混乱,从而减少回报。我只在全球范围内使用。

所以对我来说,const应该保留给具有真正不同语义方法分派(通常由缺少const-copy构造函数表示)或全局的对象。否则,这只是为了小利益或没有利益。


当然,至少对于需要长期保持可维护性的代码来说是这样的。

如果您想了解更多有关这方面的信息,Scott Meyers的"有效C++"显示了您需要const返回类型和方法参数来保护代码免遭滥用的恶劣场景。