免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 52139 | 回复: 120
打印 上一主题 下一主题

const,C/C++的第一个败笔 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-01-04 17:16 |只看该作者 |正序浏览
经过越来越多的教训,偶不得不说,const,是C/C++历史上最失败的发明

首先,const侮辱了程序员的智商。C/C++的精髓之一既是,相信程序员的能力,
const却说,你程序员是容易出错的,我来帮你编译时把把关。

const一边侮辱着程序员的智商,一般又出尔反尔,笑里藏刀般给出某种妥协,
对了,还有强制类型转换,还有const_cast。你看到的是const,未必是const的。

其次,const严重削弱了编写C++代码的乐趣。写个
Object &GetSubObject()吧,不行,你可能还要再写一个
Object const &GetSubObject() const,
你竟然发现两个代码一样呢,多么愚蠢的做法!

不知道发明STL的人,有没对const恨的咬牙切齿呢!
当我第一次接触STL,发现iterator很好用,
但是当我再看到const_interator时,我想STL的作者一定在牙痒吧。。
当再看到又冒出来一个const_reserver_interator时,心想如果我是STL的作者,一定会咆哮了。。

如果要写C++ template,就更糟糕了,对于类型int const和int,
竟然需要不同的偏特化。偏特化的代码,几乎一样吧,没错了,
都是const惹的祸。

最新写一个templatge,对 pair<A, B>做偏特化,
当我发现不得不对
pair<A,B>
pair<const A, B>
pair<A, const B>
const pair<A, B>
const pair<const A, B>
const pair<A, const B>
六种情况逐一处理时, 我彻底的崩溃了。。

const,你就不能安静的移出C/C++标准吗?

[ 本帖最后由 芙蓉 于 2008-1-4 17:40 编辑 ]

评分

参与人数 1可用积分 +5 收起 理由
langue + 5 原创内容

查看全部评分

论坛徽章:
0
121 [报告]
发表于 2011-07-28 16:28 |只看该作者
1、喜欢这样的讨论,能学到一些东西。
2、接触C++之前,在C中很少用const。
3、C++后,发现const很烦。
4、没有过多的质疑,它存在有存在的道理。

论坛徽章:
0
120 [报告]
发表于 2011-04-05 14:26 |只看该作者
const如果滥用其实真会恶心人,所以只要能加尽量加的说法不对。
但const的最重要用途是作为参数修饰,比如接口void foo(A& a) 判断调用完该函数a是否有改变那是相当饶头的事情。而 void foo(const A& a),我们就比较自信a不会改变了(虽然可能函数的第一步是const_cast)。
const只是个君子协定,拯救不了疯狂的程序员,但大多数场景下有助于理解接口和用法,故能方便编程。所以还是利大于弊的。

论坛徽章:
0
119 [报告]
发表于 2011-04-05 12:56 |只看该作者
能说这样的话,楼主可以去C++标准委员会了

论坛徽章:
0
118 [报告]
发表于 2011-04-04 18:57 |只看该作者
LZ读书太少了,项目也少,你太高估程序员的能力了!!!
去好好读读《effective C++ 》 by scott meyers 里面的条款。
若有第3版,请读第3条。

下面是从网上摘抄的。
-------------------------

第3条:尽可能使用 const
const令人赞叹之处就是:你可以通过它来指定一个语义上的约束(一个特定的不能够更改的对象)这一约束由编译器来保证。通过一个const,你可以告诉编译器和其他程序员,你的程序中有一个数值需要保持恒定不变。不管何时,当你需要这样一个数时,你都应该这样做,这样你便可以让编译器来协助你确保这一约束不被破坏。
const 关键字的用途十分广泛。在类的外部,你可以定义全局的或者名字空间域的常量,也可以通过添加 static 关键字来定义文件、函数、或者程序块域的对象。在类的内部,你可以使用它来定义静态的或者非静态的数据成员。对于指针,你可以制定一个指针是否是 const 的,其所指的数据是否是 const 的,或者两者都是 const ,或者两者都不是。
char greeting[] = "Hello";
char *p = greeting;                    // 非 const 指针,非 const 数据
const char *p = greeting;              // 非 const 指针, const 数据
char * const p = greeting;             // const 指针,非 const 数据
const char * const p = greeting;       // const 指针, const 数据
这样的语法看上去反复无常,实际上并不是这样。如果 const 关键字出现在星号的左边,那么指针所指向的就是一个常量;如果 const 出现在星号的右边,那么指针本身就是一个常量;如果 const 同时出现在星号的两边,那么两者就都是常量。
当所指向的为常量时,一些程序员喜欢把 const 放在类型之前;其他一些人则喜欢放在类型后边,但要在星号的前边。这两种做法没有什么本质的区别,所以下边给出的两个函数声明的参数表实际上是相同的:
void f1(const Widget *pw);  // f1 传入一个指向 Widget 对象常量的指针
void f2(Widget const *pw);  // f2 也一样
由于这两种形式在实际代码中都会遇到,所以你都要适应。
STL 迭代器是依照指针模型创建的, 所以说一个 iterator 更加像一个指向 T* 的指针。把一个 iterator 声明为 const 的更像是声明一个 const 的指针(也就是声明一个指向 T* const 的指针): iterator 不允许指向不同类型的内容,但是其所指向的内容可以被修改。如果你希望一个迭代器指向某些不能被修改的内容(也就是指向 const T* 的指针),此时你需要一个 const_iterator :
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
                                  // iter 就像一个 T* const
*iter = 10;                       // 正确,可以改变 iter 所指向的内容
++iter;                           // 出错! Iter 是一个 const
std::vector<int>::const_iterator cIter = vec.begin();
                                  // cIter 就像一个 const T*
*cIter = 10;                      // 出错! *cIter 是一个 const
++cIter;                          // 正确,可以改变 cIter
const 在函数声明方面还有一些强大的用途。在一个函数声明的内部, const 可以应用在返回值、单个参数,对于成员函数,可以将其本身声明为 const 的。
让函数返回一个常量通常可以减少意外发生的可能,而且不用放弃考虑安全和效率问题。好比有理数乘法函数( operator* )的声明,更多信息请参见第 24 项。
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
很多程序员在初次到这样的代码时都不会正眼看一下。为什么 operator* 要返回一个 c onst 对象呢?这是因为如果不是这样,客户端将会遇到一些不愉快的状况,比如:
Rational a, b, c;
...
(a * b) = c;                  // 调用 operator= 能返回一个 a*b !
我不知道为什么一些程序员会企图为两个数 的乘积赋值,但是我确实知道好多程序员的初衷并非如此。他们也许仅仅在录入的时候出了个小差错(他们的本意也许是一个布尔型的表达式):
if (a * b = c) ...            // 噢 … 本来是想进行一次比较!
显而易见,如果 a 和 b 是内建数据类型,那么这样的代码就是非法的。避免与内建数据类型不必要的冲突,这是一个优秀的用户自定义类型的设计标准之一(另请参见第 18 项),而允许为两数乘积赋值这让人看上去就很不必要。声明 operator* 函数时如果让其返回一个 const 型数据则可以避免这一冲突,这便是要这样做的原因所在。
const 的参数没有什么特别新鲜的——它们与局部 const 对象的行为基本一致,你在必要的时候要尽可能使用它们。除非你需要更改某个参数或者局部对象,其余的所有情况最好都声明为 const 。这仅仅需要你多打六个字母,但是它可以使你从恼人的错误(比如我们刚才见到的“我本想打‘ == ’但是却打了‘ = ’”)中解放出来。
const 成员函数
对成员函数使用 const 的目的是指明这些成员函数可以被 const 对象调用。这一类成员函数是很重要的,首先,它们使得类的接口更加易于理解。很有必要了解哪些函数可以修改而哪些不可以。其次,这些函数可以与 const 对象协同工作。这对于高效编码是十分重要的一方面,这是由于(将在第 20 项中展开解释)提高 C++ 程序性能的一条最基本的途径就是:传递对象的 const 引用。使用这一技 术需要一个前提:这就是首先要有 const 成员函数存在,并且它们用于处理之前生成的 const 对象。
如果若干成员函数之间的区别仅仅为“是否是 const 的”,那么它们也可以被重载。很多人都忽略了这一点,但是这是 C++ 重要特征之一。请观察下面的代码,这是一个文字块的类:
class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const
                                // operator[] 用于返回相应位置的字符
  { return text[position]; }     // 返回一个 const 对象
  char& operator[](std::size_t position)
                                // operator[] 用于返回相应位置的字符
  { return text[position]; }     // 返回一个非 const 对象
private:
   std::string text;
};
TextBlock 的 operator[] 可以这样使用:
TextBlock tb("Hello";
std::cout << tb[0];        // 调用非 const 的 TextBlock:perator[]
const TextBlock ctb("World";
std::cout << ctb[0];       // 调用 const 的 TextBlock:perator[]
顺便说一下,在真实的程序中, const 对象在大多数情况下都以“通过指针传递”或“引用一个 const ”的形式出现。 上面的 ctb 的例子纯粹是人为的,而下面的例子在真实状况中常会出现:
void print(const TextBlock& ctb)       // 在这个函数中 ctb 是 const 的
{
  std::cout << ctb[0];        // 调用 const 的 TextBlock:perator[]
  ...
}
通过对 operator[] 的重载以及为每个版本提供不同类型的返回值,你便可以以不同的方式处理 const 的或者非 const 的 TextBlock :
std::cout << tb[0];           // 正确:读入一个非 const 的 TextBlock
tb [0] = 'x';                  // 正确:改写一个非 const 的 TextBlock
std::cout << ctb[0];          // 正确:读入一个 const 的 TextBlock
ctb [0] = 'x';                 // 错误 ! 不能改写 const 的 TextBlock
请注意,这一错误只与所调用的 operator[] 的返回值的类型有关,如果仅仅调用 operator[] 本身则不会出现任何问题。错误出现在:企图为一个 const char& 赋值,而 const char& 则是 operator[] 的 const 版本的返回值类型。
同时还要注意的是,非 const 的 operator[] 的返回值类型是一个 char 的引用,而不是 char 本身。如果 operator[] 真的简单的返回一个 char ,那么下面的语句将不能正确编译:
tb[0] = 'x';
这是因为,企图修改一个返回内建数据类型的函数的返回值根本都是非法的。即使假设这样做合法,而 C++ 是 通过传值返回对象的,所修改的仅仅是由 tb.text[0] 复制出的一份副本,而不是 tb.text[0] 本身,你不会得到预期的效果。
让我们暂停一小会儿,来考虑一下这里边的哲学问题。把一个成员函数声明为 const 的有什么涵义呢?这里有两个流行的说法:按位恒定(也可叫做物理恒定)和逻辑恒定。
按位 恒定阵营坚信:当且仅当一个成员函数对于所有对象的数据成员( static 数据成员除外)都不做出改动时,才需要将这一成员函数声明为 const 的,换句话说,将成员函数声明为 const 的条件是:成员函数不对对象内部做任何的改动。按位恒定的好处之一就是,它使得错误检查便得更轻松:编译器仅需要查找对数据成员的赋值。实际上,按位恒定就是 C++ 对于恒定的定义,如果一个 const 的成员函数调用了某个对象,那么即使该对象拥有非静态数据成员,其所有数据成员也都是不可修改的。
不幸的是,大多数不完全是 const 的成员函数也可以通过按位恒定的检验。在特定的情况下,如果一个成员函数频繁的修改一个指针所指的位置,那么我们说它就不是一个 const 的成员函数。但是只要这个指针存在于一个对象中,这个函数就是按位恒定的,这时候编译器不会报错。这样会导致编成的行为不符合常规习惯。比如说,我们手头有一个类似于 TextBlock 的类,其中保存着 char* 类型的数据而不是 string ,因为这段代码有可能要与一些 C 语言的 API 交互,但是 C 语言中没有 string 对象一说。
class CTextBlock {
public:
  ...
  char& operator[](std::size_t position) const
  // operator[] 不恰当的 (但是符合按位恒定规则)定义方法
  { return pText[position]; }
private:
  char *pText;
};
尽管 operator[] 返回一个对对象内部数据的引用,这个类仍(不恰当地)将其声明为 const 的 成员函数(第 28 项将深入讨论这个问题)。先忽略这个问题,请注意 operator[] 的实现中并没有以任何形式修改 pText 。于是编译器便会欣然接受这样的做法,毕竟,所有的编译器所检查的是“代码是否符合按位恒定规则”。但是请观察,在编译器的纵容下,还会有什么样的事情发生:
const CTextBlock cctb("Hello";// 声明对象常量
char *pc = &cctb[0];            // 调用 const 的 operator[]
                                // 从而得到一个指向 cctb 中数据的指针
*pc = 'J';                      // cctb 现在的值为 "Jello"
当你创建了一个包含具体值的对象常量后,你仅仅通过对其调用 const 的成员函数,就可以改变它的值!这显然是有问题的。
辑恒定应运而生。坚持这一宗旨的人们争论道,一个 const 的成员函数可能对其调用的对象内部做出改动,但是仅仅以客户端无法察觉的方式进行。比如说,你的 CTextBlock 类可能需要保存文字块的长度,以便在需要的时候调用:
class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  std::size_t textLength;      // 最后一次计算出的文字块长度
  bool lengthIsValid;          // 当前长度是否可用
};
std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
textLength = std::strlen(pText);    // 错误!不能在 const 成员函数中
lengthIsValid = true;          // 对 textLength 和 lengthIsValid 赋值
  }
  return textLength;
}
以上 length 的实现绝不是按位恒定的。这是因为 textLength 和 lengthIsValid 都可以改动。尽管看上去它应该对于 CTextBlock 对象常量可用,但是编译器不答应。编译器始终坚持遵守按位恒定。那么该怎么办呢?
解决方法很简单:利用 C++ 中 与 const 相关的灵活性,使用可变的( mutable )数据成员。 mutable 可以使非静态数据成员不受按位恒定规则的约束:
class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  mutable std::size_t textLength;// 这些数据成员在任何情况下均可修改
  mutable bool lengthIsValid;     // 在 const 成员函数中也可以
};
std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);    // 现在可以修改了
    lengthIsValid = true;               // 同上
  }
  return textLength;
}
避免 const 与非 const 成员函数之间的重复
mutable 对于“我不了解按位恒定”的情况不失为一个良好的解决方案,但是它对于所有的 const 难题并不能做到一劳永逸。举例说, TextBlock (以及 CTextBlock )中的 operator[] 不仅仅返回一个对恰当字符的引用,同时还要进行边界检查、记录访问信息,甚至还要进行数据完整性检测。如果将所有这些统统放在 const 或非 const 函数(我们现在会得到过于冗长的隐式内联函数,不过不要惊慌,在第 30 项中这个问题会得到解决)中,看看我们会得到什么样的庞然大物:
class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const
  {
    ...                                 // 边界检查
    ...                                 // 记录数据访问信息
    ...                                 // 确认数据完整性
    return text[position];
  }
  char& operator[](std::size_t position)
  {
    ...                                 // 边界检查
    ...                                 // 记录数据访问信息
    ...                                 // 确认数据完整性
    return text[position];
  }
private:
   std::string text;
};
噢!天哪,这让人头疼:重复代码,以及随之而来的编译时间增长、维护成本增加、代码膨胀、等等……当然,像边界检查这一类代码是可以移走的,它们可以单独放在一个成员函数(当然是私有的)中,然后让这两个版本的 operator[] 来调用它,但是你的代码仍然有重复的函数调用,以及重复的 return 语句。
对于 operator[] 你真正需要的是:一次实现,两次使用。也就是说,你需要一个版本的 operator[] 来调用另一个。这样便可以通过转型来消去函数的恒定性。
通常情况下转型是一个坏主意,后边我将专门用一项来告诉你为什么不要使用转型(第 21 项),但是代码重复也不会让人感到有多轻松。在这种情况下, const 版的 operator[] 与非 const 版的 operator[] 所做的事情完全相同,不同的仅仅是它的返回值是 const 的。通过转型来消去返回值的恒定性是安全的,这是因为任何人调用这一非 const 的 operator[] 首先必须拥有一个非 const 的对象,否则它就不能调用非 const 函数。所以尽管需要一次转型,在 const 的 operator[] 中调用非 const 版本,可以安全地避免代码重复。下面是实例代码,读完后边的文字解说你会更明了。
class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const     // 同上
  {
    ...
    return text[position];
  }
  char& operator[](std::size_t position)  // 现在仅调用 const 的 op[]
  {
    return
      const_cast<char&>(       // 通过对 op[] 的返回值进行转型,消去 const ;
        static_cast<const TextBlock&>(*this)// 为 *this 的类型添加 const ;
          [position];                      // 调用 const 版本的 op[]
      );
  }
...
};
就像你所看到的,上面的代码进行了两次转型,而不是一次。我们要让非 const 的 operator[] 去调用 const 版本的,但是如果在非 const 的 operator[] 的内部,我们只调用 operator[] 而不标明 const ,那么函数将对自己进行递归调用。那将是成千上万次的毫无意义的操作。为了避免无穷递归的出现,我们必须要指明我们要调用的是 const 版本的 operator[] ,但是手头并没有直接的办法。我们可以用 *this 从 TextBlock& 转型到 const TextBlock& 来取代。是的,我们使用了一次转型添加了一个 const !这样我们就进行了两次转型:一次为 *this 添加了 const (于是对于 operator[] 的调用将会正确地选择 const 版本),第二次转型消去了 const operator[] 返回值中的 const 。
添加 const 的那次转型是为了保证转换工作的安全性(从一个非 const 对象转换为一个 const 的),这项工作的关键字是 static_cast 。消去 const 的工作只可以通过 const_cast 来完成,所以在这里我们实际上并没有其他的选择。(从技术上讲,我们有。 C 语 言风格的转型在这里也能工作,但是,就像我在第 27 项中所讲的,这一类转型在很多情况下都不是好的选择。如果你对于 static_cast 和 const_cast 还不熟悉,第 27 项中有详细的介绍。)
在众多的示例中,我们最终选择了一个运算符来进行演示,因此上面的语法显得有些古怪。这些代码可能不会赢得任何选美比赛,但是通过以 const 版本的形式实现非 const 版本的 operator[] ,可以避免代码重复,这正是我们所期望的。为达到这一目标而写下看似笨拙的代码,这样做是否值得全看你的选择,但是,以 const 版本的形式来实现非 const 的成员函数——了解这一技术肯定是值得的。
更值得你了解的是按反方向完成上面的工作——通过让 const 版本的函数调用非 const 版本来避免代码重复——一定不要这样做。请记住,一个 const 成员函数保证其对象永远不会更改其逻辑状态,但是一个非 const 的成员函数并没有这一类的保证。如果你在一个 const 函数中调用了一个非 const 函数,曾保证不会被改动的对象就有被修改的风险。这就是为什么说让一个 const 函数调用一个非 const 函数是错误的:对象有可能被修改。实际上,为了使代码能够得到编译,你还需要使用一个 const_cast 来消去 *this 的 const 属性,显然这是不必要的麻烦。上一段中相反的调用次序才是安全的:非 const 成员函数可以对一个对象做任何想做的事情,因此调用一个 const 成员函数不会带来任何风险。这就是为什么 static_cast 在没有与 const 相关的危险的情况下可以正常工作的原因。
就像本项最开始所说的, const 是一个令人赞叹的东西。对于指针和迭代器,以及指针、迭代器和引用所涉及的对象,函数的参数和返回值,局部变量,成员函数来说, const 都是一个强大的伙伴。只要可能就可以使用它。你会对你所做的事情感到高兴的。
需要记住的
将一些东西声明为 const 的可以帮助编译器及时发现用法上的错误。 const 针对对象作用于所有的作用域,针对函数参数和返回值、成员函数作用于整体。
编译器严格遵守按位恒定规则,但是你应该在需要时应用逻辑恒定。
当 const 和非 const 成员函数的实现在本质上相同时,可以通过使用一个非 const 版本来调用 const 版本来避免代码重复。

论坛徽章:
0
117 [报告]
发表于 2011-02-25 08:49 |只看该作者
回复 114# OwnWaterloo


    嗯,正是,呵呵!来这里学习了!

论坛徽章:
0
116 [报告]
发表于 2011-02-24 10:27 |只看该作者
其实编程就应该拿磁铁在硬盘上画
用十六进制编辑器、编译器啥的,都是侮辱程序员的智商

论坛徽章:
1
2015年迎新春徽章
日期:2015-03-04 09:56:11
115 [报告]
发表于 2011-02-24 09:56 |只看该作者
经过越来越多的教训,偶不得不说,const,是C/C++历史上最失败的发明

首先,const侮辱了程序员的智商。C ...
芙蓉 发表于 2008-01-04 17:16



    我们可以人为规避一些不用的特性,如果你觉得无用话。

论坛徽章:
0
114 [报告]
发表于 2011-02-23 23:22 |只看该作者
C/C++的精髓之一既是,相信程序员的能力
---------------------------------------------
程序员是完全不可靠的。。人都会出错

论坛徽章:
2
青铜圣斗士
日期:2015-11-26 06:15:59数据库技术版块每日发帖之星
日期:2016-07-24 06:20:00
113 [报告]
发表于 2011-02-23 14:24 |只看该作者
回复 113# cexer

这……   cppblog的cexer?
  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP