为什么const-correctness特定于C++?

Asi*_*sik 40 c++ const const-correctness

免责声明:我知道有两个关于const-correctness有用的问题,但是,没有人讨论过如何在C++中使用const-correctness 而不是其他编程语言.此外,我对这些问题的答案不满意.

我现在使用了一些编程语言,在C++中让我烦恼的一件事就是const-correctness的概念.在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());
}
Run Code Online (Sandbox Code Playgroud)

Visual Studio接受使用const注释掉的编译,但是使用警告C4239时,使用非标准扩展.所以,如果你想要传递临时代码,避免副本和保持标准兼容的语法简洁,你必须通过const引用,不管它.不过,这更像是一个怪癖,而不是一个根本原因.

否则,实际上不存在必须使用const的情况,除非与使用const的其他代码连接.在我看来,康斯特似乎不是一个自以为是的瘟疫,它蔓延到它接触到的一切:

const在C++中工作的原因是因为你可以把它丢弃.如果你不能把它扔掉,那么你的世界就会糟透了.如果声明一个采用const Bla的方法,则可以将它传递给非const Bla.但如果是相反的方式你不能.如果声明一个采用非const Bla的方法,则不能将它传递给const Bla.所以现在你被卡住了.所以你逐渐需要一个不是const的所有东西的const版本,你最终得到了一个阴影世界.在C++中,你可以使用它,因为与C++中的任何东西一样,无论你是否想要这个检查,它都是纯粹可选的.如果你不喜欢它,你可以打破常量.

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

Mic*_*and 63

Const正确性为我能想到的C++提供了两个显着的优点,其中一个使它非常独特.

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

C++不是唯一具有const正确性的语言或类似的东西.OCaml和标准ML有一个类似的概念,使用不同的术语 - 几乎所有数据都是不可变的(const),当你想要某些东西是可变的时,你可以使用不同的类型(一种ref类型)来实现它.所以它在其邻近语言中只是C++的独特之处.

最后,走向另一个方向:曾经有一段时间我想在Java中使用const. final有时甚至不能创建明显不可变的数据(特别是可变数据的不可变视图),并且不想创建接口.查看Java API中的Unmodifiable集合支持,以及它只在运行时检查是否允许修改const为什么有用的示例(或者至少接口结构应该深入到List和MutableList) - 那里没有理由试图改变不可变结构不能是编译类型错误.

  • 重要的一点是,它允许您表达不能用其他语言表达的不变性概念(即使建议使用) (10认同)
  • "在实践中你可以[在大多数环境中安全地施放常规"".不完全是.修改定义为"const"的对象的UB不限于它可能在ROM中的风险.如果你写`const int i = 1; 突变((INT*)&ⅰ); std :: cout << i;`然后允许编译器假设程序中的"mutate"和其他任何东西都没有通过写入`i`来调用UB.在实践中,您希望修改对于该代码要么受到尊重要么被无害地忽略,但一般情况下,如果优化器假设某些事情不是真的,那么各种各样的事情都可能出错. (2认同)

Nem*_*vic 30

我不认为任何人声称const-correctness是"必要的".但同样,课程也不是真的有必要,是吗?命名空间,异常也是如此......你得到了图片.

Const-correctness有助于在编译时捕获错误,这就是它有用的原因.


Lou*_*nco 16

const是一种表达某种东西的方式.如果您认为表达它很重要,那么它在任何语言中都会有用.他们没有这个功能,因为语言设计师没有发现它们有用.如果这个功能在那里,我认为这将是有用的.

我有点像Java中的throw规范.如果你喜欢它们,你可能会喜欢其他语言.但是其他语言的设计者并不认为它那么重要.

  • 与检查异常的类比是一个很好的 - 你打败了我! (4认同)
  • 这击中了头上的钉子.const关键字为程序员提供语义含义"我没有改变任何东西!" (4认同)

Jer*_*ner 13

你说得对,const-correctness不是必需的.您当然可以在没有const关键字的情况下编写所有代码并使其工作正常,就像在Java和Python中一样.

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

因此,试图颠覆或避免使用const-correctness功能只会让事情从长远来看变得更加困难.


Asi*_*sik 12

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

C++具有"const-correctness"而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);
}
Run Code Online (Sandbox Code Playgroud)

这段代码应该怎么做?

  1. 无法编译
  2. 打印"3,4"
  3. 打印"0,0"

答案是#3.C#尝试通过调用对象的丢弃副本Add方法来尊重"readonly"关键字.这很奇怪,是的,但它有什么其他选择?如果它调用原始Vector上的方法,则对象将发生更改,从而违反字段的"readonly".如果它无法编译,那么readonly值类型成员就没用了,因为你不能在它们上面调用任何方法,因为它们可能会改变对象.

如果我们只能标记在readonly实例上调用哪些方法是安全的......等等,这正是C++中的const方法!

C#没有使用const方法,因为我们不使用C#中的值类型; 我们只是避免可变的值类型(并宣布他们为"邪恶的",看到1,2).

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

C++没有"引用类型"的概念,只有"值类型"(在C#-lingo中); 其中一些值类型可以是指针或引用,但与C#中的值类型一样,它们都不拥有它们的存储空间.如果C++在其类型上处理"const"的方式与C#在值类型上处理"readonly"的方式一样,那将会非常混乱,如上面的示例所示,从未与复制构造函数进行讨厌的交互.

因此,C++不会创建一个丢弃的副本,因为这会产生无穷无尽的痛苦.它也不禁止你在成员上调用任何方法,因为,那么语言不会非常有用.但它仍然希望有一些"只读"或"常识"的概念.

C++尝试通过标记哪些方法可以安全地调用const成员来找到一种中间方式,然后它相信您在标记中忠实且准确,并直接调用原始对象上的方法.这并不完美 - 它很冗长,你可以随意违反常规 - 但它可以说比所有其他选项更好.


Dav*_*eas 8

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

Constantness使您能够从设计板到代码清楚地表达其他语言缺乏的概念.当你来自一种没有它的语言时,你可能会对你从未使用过的概念感到困惑 - 如果你以前从未使用它,它有多重要?

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

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

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

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

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

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

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

作为旁注,当您处理引用时(在C++引用或指针中),Java中的final关键字与C++中的const关键字无关.final关键字修改它引用的局部变量,无论是基本类型还是引用,但不是引用对象的修饰符.也就是说,您可以通过最终引用调用mutating方法,从而提供对象引用状态的更改.在C++中,引用始终是常量(在构造期间只能将它们绑定到对象/变量),而const关键字修改用户代码如何处理引用对象.(在指针的情况下,你可以使用const关键字作为基准和指针:X const*const声明一个指向常量X常量指针)

  • 我不能同意你的评论.首先,如果你定义的接口等价于const,你可以填充很多接口(和虚拟方法)来获得相同的结果,结果就像const关键字一样具有病毒性.const关键字扩展到所有内容,因为它强加的访问限制不是因为它是一个"魔术"关键字.然后我真的没有看到你的最后一点:"[const]根据上下文有许多不同的含义".const只有一个意思,或者我错过了什么? (3认同)

chr*_*mue 6

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