45

免责声明:我知道关于 const 正确性的有用性有两个问题,但是,没有人讨论与其他编程语言相比,在 C++ 中如何需要 const 正确性。另外,我对这些问题的答案不满意。

我现在已经使用了几种编程语言,而在 C++ 中困扰我的一件事是 const 正确性的概念。在 Java、C#、Python、Ruby、Visual Basic 等中没有这样的概念,这似乎是 C++ 特有的。

在您向我推荐 C++ FAQ Lite 之前,我已经阅读了它,但它并不能说服我。完全有效、可靠的程序一直都是用 Python 编写的,并且没有 const 关键字或等效关键字。在 Java 和 C# 中,对象可以声明为 final(或 const),但没有 const 成员函数或 const 函数参数。如果一个函数不需要修改一个对象,它可以采用一个只提供对该对象的读取访问的接口。该技术同样可以在 C++ 中使用。在我研究过的两个真实世界的 C++ 系统上,几乎没有在任何地方使用 const,而且一切正常。所以我对让 const 污染代码库的用处还很远。

我想知道在 C++ 中是什么使 const 成为必要,而不是其他编程语言。

到目前为止,我只看到了一种必须使用 const 的情况:

#include <iostream>

struct Vector2 {
    int X;
    int Y;
};

void display(/* const */ Vector2& vect) {
    std::cout << vect.X << " " << vect.Y << std::endl;
}

int main() {
    display(Vector2());
}

Visual Studio 接受使用注释掉的 const 进行编译,但使用警告C4239,使用非标准扩展。所以,如果你想要传递临时变量、避免复制和保持标准兼容的语法简洁,你必须通过 const 引用传递,没有办法绕过它。尽管如此,这更像是一个怪癖而不是根本原因。

否则,实际上没有必须使用 const 的情况,除非与使用 const 的其他代码交互。在我看来,Const 只不过是一种自以为是的瘟疫,它蔓延到它所触及的一切:

const 在 C++ 中起作用的原因是你可以抛弃它。如果你不能把它扔掉,那么你的世界就会糟透了。如果你声明一个接受 const Bla 的方法,你可以传递一个非常量 Bla。但是如果反过来就不行了。如果您声明一个采用非常量 Bla 的方法,则不能将其传递给 const Bla。所以现在你被卡住了。所以你逐渐需要一个非 const 的东西的 const 版本,你最终会得到一个影子世界。在 C++ 中你可以摆脱它,因为与 C++ 中的任何东西一样,无论你是否想要这个检查都是完全可选的。如果你不喜欢它,你可以把它打掉。

Anders Hejlsberg(C# 架构师),CLR 设计选择

4

14 回答 14

65

const 正确性为 C++ 提供了我能想到的两个显着优势,其中之一使它相当独特。

  • 它允许可变/不可变数据的普遍概念,而不需要一堆接口。可以注释各个方法是否可以在 const 对象上运行,并且编译器会强制执行此操作。是的,有时它可能会很麻烦,但是如果您始终如一地使用它并且不使用它,那么const_cast对于可变数据和不可变数据,您就有了经过编译器检查的安全性。
  • 如果一个对象或数据项是const,编译器可以自由地将它放在只读内存中。这在嵌入式系统中尤其重要。C++ 支持这一点;很少有其他语言可以。这也意味着,在一般情况下,您不能安全地丢弃const,尽管实际上您可以在大多数环境中这样做。

C++ 不是唯一具有 const 正确性或类似的语言。OCaml 和标准 ML 有一个相似的概念,但术语不同——几乎所有数据都是不可变的 (const),当您希望某些东西是可变的时,您可以使用不同的类型 (a reftype) 来实现这一点。所以它只是 C++ 在其相邻语言中的独特之处。

最后,转向另一个方向:有时我想要在 Java 中使用 const。 final有时还不足以创建明显不可变的数据(尤其是可变数据的不可变视图),并且不想创建接口。查看 Java API 中的 Unmodifiable 集合支持,以及它仅在运行时检查是否允许修改的事实,以举例说明为什么 const 有用(或者至少应该加深接口结构以具有 List 和 MutableList)——那里没有理由尝试改变不可变结构不能是编译类型错误。

于 2009-09-02T21:06:20.430 回答
32

我认为没有人声称 const 正确性是“必要的”。但同样,课程也不是真正必要的,不是吗?命名空间、异常等等也是如此……你明白了。

常量正确性有助于在编译时捕获错误,这就是它有用的原因。

于 2009-09-02T21:05:18.310 回答
17

好吧,我需要 6 年才能真正理解,但现在我终于可以回答我自己的问题了。

C++ 具有“常量正确性”而 Java、C# 等没有的原因是 C++ 仅支持值类型,而这些其他语言仅支持或至少默认为引用类型

让我们看看默认为引用类型的语言 C# 如何在涉及值类型时处理不变性。假设您有一个可变值类型,而另一个类型具有该类型的只读字段:

struct Vector {
    public int X { get; private set; }
    public int Y { get; private set; }
    public void Add(int x, int y) {
        X += x;
        Y += y;
    }
}

class Foo {
    readonly Vector _v;
    public void Add(int x, int y) => _v.Add(x, y);
    public override string ToString() => $"{_v.X} {_v.Y}";
}

void Main()
{
    var f = new Foo();
    f.Add(3, 4);
    Console.WriteLine(f);
}

这段代码应该做什么?

  1. 编译失败
  2. 打印“3, 4”
  3. 打印“0, 0”

答案是#3。C# 尝试通过在对象的一次性副本上调用方法 Add 来尊重您的“只读”关键字。这很奇怪,是的,但它还有其他选择吗?如果它调用原始 Vector 上的方法,则对象将发生更改,从而违反了该字段的“只读”特性。如果编译失败,那么只读值类型成员就毫无用处,因为您不能在它们上调用任何方法,因为担心它们可能会更改对象。

如果我们可以标记哪些方法可以安全地调用只读实例……等等,这正是 C++ 中的 const 方法!

C# 不关心 const 方法,因为我们在 C# 中没有太多使用值类型;我们只是避免可变值类型(并将它们声明为“邪恶”,参见12)。

此外,引用类型不会遇到这个问题,因为当您将引用类型变量标记为只读时,只读的是引用,而不是对象本身。这对编译器来说很容易强制执行,它可以将任何赋值标记为编译错误,除了在初始化时。如果您只使用引用类型并且所有字段和变量都是只读的,那么您可以在任何地方以很少的语法成本获得不变性。F# 完全像这样工作。Java 通过不支持用户定义的值类型来避免这个问题。

C++ 没有“引用类型”的概念,只有“值类型”(在 C#-lingo 中);其中一些值类型可以是指针或引用,但就像 C# 中的值类型一样,它们都没有自己的 storage。如果 C++ 在其类型上处理“const”的方式与 C# 在值类型上处理“只读”的方式相同,那么正如上面的示例所示,这将非常令人困惑,更不用说与复制构造函数的讨厌交互了。

所以 C++ 不会创建一个丢弃的副本,因为那会造成无尽的痛苦。它也不禁止您对成员调用任何方法,因为,好吧,那时该语言不会很有用。但它仍然希望有一些“只读”或“常量”的概念。

C++ 试图通过让你标记哪些方法可以安全地调用 const 成员来找到一种中间方法,然后它相信你在标记时一直忠实和准确,并直接在原始对象上调用方法。这并不完美——它很冗长,你可以随意违反 const-ness——但可以说它比所有其他选项都要好。

于 2015-08-27T17:26:16.037 回答
16

const 是一种表达方式。如果您认为表达它很重要,它将在任何语言中都很有用。他们没有这个功能,因为语言设计者没有发现他们有用。我认为,如果该功能存在,它将同样有用。

我觉得它就像 Java 中的 throw 规范。如果你喜欢它们,你可能会喜欢其他语言的它们。但其他语言的设计者并不认为这很重要。

于 2009-09-02T21:03:39.467 回答
13

你是对的,常量正确性不是必需的。你当然可以在没有 const 关键字的情况下编写所有代码并让事情正常工作,就像在 Java 和 Python 中一样。

但是如果你这样做,你将不再得到编译器的帮助来检查 const 违规。编译器会在编译时告诉您的错误现在只能在运行时找到(如果有的话),因此会花费您更长的时间来诊断和修复。

因此,从长远来看,试图颠覆或避免 const-correctness 特性只会让事情变得更难。

于 2009-09-02T21:06:14.593 回答
8

编程是用一种最终将由计算机处理的语言编写的,但这既是与计算机和同一项目中的其他程序员进行通信的一种方式。当您使用一种语言时,您受限于可以在其中表达的概念,而 const 只是您可以用来描述您的问题和解决方案的另一个概念。

恒定性使您能够清楚地表达从设计板到代码一的概念,这是其他语言所缺乏的。当您来自一门没有它的语言时,您可能会对从未使用过的概念感到困惑——如果您以前从未使用过它,它有多重要?

语言和思想是紧密结合的。你只能用你说的语言表达你的想法,但语言也会改变你的思维方式。您使用的语言中没有 const 关键字这一事实意味着您已经找到了相同问题的其他解决方案,并且这些解决方案对您来说似乎很自然。

在您认为您可以提供一个非变异接口的问题中,该接口可由不需要更改对象内容的函数使用。如果您考虑一下,这句话会告诉您为什么 const 是您要使用的概念。必须定义一个非可变接口并在您的类中实现它是一种解决方法,即您无法用您的语言表达该概念。

恒定性允许您用编译器(和其他程序员)可以理解的语言来表达这些概念。您正在就您将如何处理您收到的参数、您存储的引用或定义允许您的类的用户对您提供的引用执行的操作进行折衷。几乎每个非平凡类都可以有一个由属性表示的状态,并且在许多情况下,必须保留不变量。该语言允许您定义提供对某些内部数据的访问权限的函数,同时限制对只读视图的访问,以保证没有外部代码会破坏您的不变量。

这是我在转向其他语言时更想念的概念。考虑一个场景,您有一个类 C,其中包含一个类型为 A 的属性 a,该属性必须对外部代码可见(您的类的用户必须能够查询有关 a 的某些信息)。如果 A 的类型有任何变异操作,那么为了防止用户代码改变你的内部状态,你必须创建一个 a 的副本并返回它。类的程序员必须意识到必须执行复制并且必须执行(可能是昂贵的)复制。另一方面,如果你可以用语言表达常量,你可以只返回一个对对象的常量引用(实际上是对对象的常量视图的引用),然后只返回内部元素。这将允许用户代码调用被检查为非变异的对象的任何方法,

问题/优势,完全取决于观点,恒定性是它是病毒性的。当您提供对对象的常量引用时,只能调用那些标记为非可变的方法,并且您必须告诉编译器哪些方法具有此属性。当您声明一个方法常量时,您是在告诉编译器调用该方法的用户代码将保持对象状态。当您定义(实现)具有常量签名的方法时,编译器会提醒您您的承诺,并且实际上要求您不要在内部修改数据。

该语言使您能够告诉编译器您无法以任何其他方式表达的方法的属性,同时,当您不符合您的设计并尝试修改数据时,编译器会告诉您。

在这种情况下,永远不应使用 const_cast<> ,因为结果可能会将您带入未定义行为的领域(从语言的角度来看:对象可能位于只读内存中,从程序的角度来看:您可能会破坏其他类中的不变量)。但是,如果您阅读过 C++FAQ lite,您当然已经知道了。

附带说明一下,当您处理引用(在 C++ 引用或指针中)时,Java 中的 final 关键字与 C++ 中的 const 关键字实际上没有任何关系。final 关键字修改它所引用的局部变量,无论是基本类型还是引用,但不是被引用对象的修饰符。也就是说,您可以通过最终引用调用变异方法,从而提供所引用对象状态的更改。在 C++ 中,引用始终是常量(您只能在构造期间将它们绑定到对象/变量)并且 const 关键字修改用户代码如何处理引用的对象。(在指针的情况下,您可以对数据和指针使用 const 关键字: X const * const 声明一个指向常量 X 的常量指针

于 2009-09-02T22:25:00.067 回答
6

如果您正在使用 FLASH 或 ROM 中的数据为嵌入式设备编写程序,那么没有 const-correctness 就无法生存。它使您能够控制不同类型内存中数据的正确处理。

于 2009-09-03T07:07:40.743 回答
4

您还想在方法中使用 const 以利用返回值优化。请参阅 Scott Meyers 更有效的 C++ 项目 20。

于 2009-09-02T21:04:30.417 回答
3

Herb Sutter的这个演讲和视频解释了关于线程安全的新内涵。 const

Constness之前你可能不必担心太多,但是使用 C++11 如果你想编写线程安全的代码,你需要了解constmutable

于 2013-01-18T10:33:19.113 回答
2

在 C、Java 和 C# 中,您可以通过查看调用站点来判断传递的对象是否可以被函数修改:

  • 在 Java 中,你知道它绝对可以。
  • 在 C 中,您知道只有在存在“&”或等效项时才可以。
  • 在 c# 中,您也需要在呼叫站点说“ref”。

在 C++ 中,您通常无法判断这一点,因为非常量引用调用看起来与按值传递相同。拥有 const 引用允许您设置和执行 C 约定。

这可以对调用函数的任何代码的可读性产生相当大的影响。这可能足以证明语言功能的合理性。

于 2009-09-02T21:19:05.553 回答
2

Anders Hejlsberg(C# 架构师):...如果您声明一个采用非常量 Bla 的方法,则不能将其传递给 const Bla。所以现在你被卡住了。所以你逐渐需要一个非 const 的东西的 const 版本,你最终会得到一个影子世界。

再说一遍:如果您开始在某些方法中使用“const”,您通常会被迫在大多数代码中使用它。但是,在代码中维护(键入、在缺少某些 const 时重新编译等)的 const 正确性所花费的时间似乎比修复由于根本不使用 const 正确性而导致的可能(非常罕见的)问题要多。因此,现代语言(如 Java、C#、Go 等)缺乏 const 正确性支持可能会导致相同代码质量的开发时间略有减少。

自 1999 年以来,Java 社区进程中存在实现 const 正确性的增强请求票,但由于上述“const 污染”以及兼容性原因,于 2005 年关闭:http: //bugs.sun.com/bugdatabase/view_bug.do ?bug_id=4211070

虽然 C# 语言没有 const 正确性构造,但类似的功能可能很快就会出现在 .NET Framework 的“Microsoft Code Contracts”(库 + 静态分析工具)中,使用 [Pure] 和 [Immutable] 属性:C#中的纯函数

于 2013-01-17T16:16:46.567 回答
2

实际上,它不是……不完全,无论如何。

在其他语言中,尤其是函数式或混合语言,如 Haskell、D、Rust 和 Scala,您有可变性的概念:变量可以是可变的,也可以是不可变的,并且通常默认情况下是不可变的。

这让你(和你的编译器/解释器)更好地理解函数:如果你知道一个函数只接受不可变的参数,那么你就知道这个函数不会改变你的变量并导致错误。

C 和 C++ 使用 const 做类似的事情,只是它的保证不那么牢固:不强制执行不变性;调用堆栈下方的函数可能会抛弃常量,并改变您的数据,但这将是故意违反 API 合同。因此,意图或最佳实践是让它像其他语言中的不变性一样工作。

尽管如此,C++ 11 现在有了一个实际的 mutable 关键字,以及更有限的 const 关键字。

于 2014-04-18T23:09:57.297 回答
1

C++ 中的 const 关键字(应用于参数和类型声明)是为了防止程序员在这个过程中大吃一惊并拿出他们的整条腿。

基本思想是将某些内容标记为“无法修改”。不能修改 const 类型(默认情况下)。const 指针不能指向内存中的新位置。很简单,对吧?

嗯,这就是 const 正确性的用武之地。以下是您在使用 const 时可以找到的一些可能的组合:

一个 const 变量 意味着由变量名标记的数据不能被修改。

指向 const 变量 的指针意味着可以修改指针,但不能修改数据本身。

指向变量的 const 指针 意味着不能修改指针(指向新的内存位置),但可以修改指针指向的数据。

指向 const 变量的 const 指针 意味着既不能修改指针也不能修改它指向的数据。

你看到那里有些事情是多么愚蠢吗?这就是为什么当你使用 const 时,重要的是要正确标记你使用的 const。

关键是这只是一个编译时破解。标签只是告诉编译器如何解释指令。如果你抛弃 const,你可以做任何你想做的事。但是您仍然必须调用具有 const 要求的方法,并使用适当转换的类型。

于 2009-09-02T21:02:27.037 回答
0

例如你有一个函数:

void const_print(const char* str)
{
    cout << str << endl;
}

另一种方法

void print(char* str)
{
    cout << str << endl;
}

主要:

int main(int argc, char **argv)
{
    const_print("Hello");
    print("Hello");        // syntax error
}

这是因为“hello”是一个 const char 指针,所以(C 风格)字符串被放入只读内存中。但是当程序员知道值不会被更改时,它总体上很有用。所以得到编译器错误而不是分段错误。就像在不需要的任务中一样:

const int a;
int b;
if(a=b) {;} //for mistake

因为左操作数是一个 const int。

于 2012-02-16T23:33:44.720 回答