科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件C++箴言:只要可能就用const

C++箴言:只要可能就用const

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

不幸的事,很多成员函数并不能完全通过二进制位常量性的检验。特别是,一个经常改变一个指针指向的内容的成员函数。

来源:中国软件网 2008年4月1日

关键字: const C++ C Linux

  • 评论
  • 分享微博
  • 分享邮件
不幸的事,很多成员函数并不能完全通过二进制位常量性的检验。特别是,一个经常改变一个指针指向的内容的成员函数。除非这个指针在这个对象中,否则这个函数就是二进制位 const 的,编译器也不会提出异议。例如,假设我们有一个类似 TextBlock 的类,因为它需要与一个不知 string 为何物的 C API 打交道,所以它需要将它的数据存储为 char* 而不是 string。

class CTextBlock {
  public:
   ...
   char& operator[](std::size_t position) const // inappropriate (but bitwise

   { return pText[position]; } // const) declaration of
   // operator[]
  private:
   char *pText;
};
  尽管 operator[] 返回对象内部数据的引用,这个类还是(不适当地)将它声明为 const 成员函数(Item 28 将谈论一个深入的主题)。先将它放到一边,看看 operator[] 的实现,它并没有使用任何手段改变 pText。结果,编译器愉快地生成了 operator[] 的代码,因为对所有编译器而言,它都是二进制位 const 的,但是我们看看会发生什么: const CTextBlock cctb("Hello"); // declare constant object

char *pc = &cctb[0]; // call the const operator[] to get a
// pointer to cctb’s data

*pc = ’J’; // cctb now has the value "Jello"
  这里确实出了问题,你用一个确定的值创建一个常量对象,然后你只是用它调用了 const 成员函数,但是你改变了它的值! 这就引出了逻辑常量性的概念。这一理论的信徒认为:一个 const 成员函数被调用的时候可能会改变对象中的一些二进制位,但是只能用客户无法感觉到的方法。例如,你的 CTextBlock 类在需要的时候可以储存文字块的长度: class CTextBlock {
  public:
   ..
   std::size_t length() const;

  private:
   char *pText;
   std::size_t textLength; // last calculated length of textblock
   bool lengthIsValid; // whether length is currently valid
};

std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
   textLength = std::strlen(pText); // error! can’t assign to textLength
   lengthIsValid = true; // and lengthIsValid in a const
  } // member function
  return textLength;
}
  length 的实现当然不是二进制位 const 的—— textLength 和 lengthIsValid 都可能会被改变——但是它还是被看作对 const CTextBlock 对象有效。但编译器不同意,它还是坚持二进制位常量性,怎么办呢?

  解决方法很简单:利用以关键字 mutable 为表现形式的 C++ 的 const-related 的灵活空间。mutable 将 non-static 数据成员从二进制位常量性的约束中解放出来: class CTextBlock {
  public:
   ...
   std::size_t length() const;

  private:
   char *pText;
   mutable std::size_t textLength; // these data members may
   mutable bool lengthIsValid; // always be modified, even in
}; // const member functions

std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
   textLength = std::strlen(pText); // now fine
   lengthIsValid = true; // also fine
  }
  return textLength;
}
  避免 const 和 non-const 成员函数的重复

  mutable 对于解决二进制位常量性不太合我的心意的问题是一个不错的解决方案,但它不能解决全部的 const-related 难题。例如,假设 TextBlock(包括 CTextBlock)中的 operator[] 不仅要返回一个适当的字符的引用,它还要进行边界检查,记录访问信息,甚至数据完整性确认,将这些功能加入到 const 和 non-const 的 operator[] 函数中,使它们变成如下这样的庞然大物: class TextBlock {
  public:
   ..
   const char& operator[](std::size_t position) const
   {
    ... // do bounds checking
    ... // log access data
    ... // verify data integrity
    return text[position];
   }
   char& operator[](std::size_t position)
   {
    ... // do bounds checking
    ... // log access data
    ... // verify data integrity
    return text[position];
   }
  private:
   std::string text;
};
  哎呀!你是说重复代码?还有随之而来的额外的编译时间,维护成本以及代码膨胀等令人头痛的事情吗?当然,也可以将边界检查等全部代码转移到一个单独的成员函数(当然是私有的)中,并让两个版本的 operator[] 来调用它,但是,你还是要重复写出调用那个函数和返回语句的代码。

  怎样才能只实现一次 operator[] 功能,又可以使用两次呢?你可以用一个版本的 operator[] 去调用另一个版本。并通过强制转型去掉常量性。

  作为一个通用规则,强制转型是一个非常坏的主意,我将投入整个一个 Item 来告诉你不要使用它,但是重复代码也不是什么好事。在当前情况下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,仅有的不同是它有一个 const 返回类型。在这种情况下,通过转型去掉返回类型的常量性是安全的,因为,无论谁调用 non-const operator[],首要条件是有一个 non-const 对象。否则,他不可能调用一个 non-const 函数。所以,即使需要一个强制转型,让 non-const operator[] 调用 const 版本以避免重复代码的方法也是安全的。代码如下,随后的解释可能会让你对它的理解更加清晰: class TextBlock {
  public:
   ...
   const char& operator[](std::size_t position) const // same as before
   {
    ...
    ...
    ...
    return text[position];
   }
   char& operator[](std::size_t position) // now just calls const op[]
   {
    return
    const_cast<char&>( // cast away const on
    // op[]’s return type;
    static_cast<const TextBlock&>(*this) // add const to *this’s type;
    [position] // call const version of op[]
   );
  }
  ...
};
  正如你看到的,代码中有两处强制转型,而不止一处。我们让 non-const operator[] 调用 const 版本,但是,如果在 non-const operator[] 的内部,我们仅仅调用了 operator[],那我们将递归调用我们自己一百万次甚至更多。为了避免无限递归,我们必须明确指出我们要调用 const operator[],但是没有直接的办法能做到这一点,于是我们将 *this 从它本来的类型 TextBlock& 强制转型到 const TextBlock&。是的,我们使用强制转型为它加上了 const!所以我们有两次强制转型:第一次是为 *this 加上 const(目的是当我们调用 operator[] 时调用的是 const 版本),第二次是从 const operator[] 的返回值之中去掉 const。

  增加 const 的强制转型是一次安全的转换(从一个 non-const 对象到一个 const 对象),所以我们用 static_cast 来做。去掉 const 的强制转型可以用 const_cast 来完成,在这里我们没有别的选择。

  在完成其它事情的基础上,我们在此例中调用了一个操作符,所以,语法看上去有些奇怪。导致其不会赢得选美比赛,但是它通过在 const 版本的 operator[] 之上实现其 non-const 版本而避免重复代码的方法达到了预期的效果。使用丑陋的语法达到目标是否值得最好由你自己决定,但是这种在一个 const 成员函数的基础上实现它的 non-const 版本的技术却非常值得掌握。

  更加值得知道的是做这件事的反向方法——通过用 const 版本调用 non-const 版本来避免代码重复——是你不能做的。记住,一个 const 成员函数承诺不会改变它的对象的逻辑状态,但是一个 non-const 成员函数不会做这样的承诺。如果你从一个 const 成员函数调用一个 non-const 成员函数,你将面临你承诺不会变化的对象被改变的风险。这就是为什么使用一个 const 成员函数调用一个 non-const 成员函数是错误的,对象可能会被改变。实际上,那样的代码如果想通过编译,你必须用一个 const_cast 来去掉 *this 的 const,这样做是一个显而易见的麻烦。而反向的调用——就像我在上面的例子中用的——是安全的:一个 non-const 成员函数对一个对象能够为所欲为,所以调用一个 const 成员函数也没有任何风险。这就是 static_cast 可以在这里工作的原因:这里没有 const-related 危险。

  就像在本文开始我所说的,const 是一件美妙的东西。在指针和迭代器上,在涉及对象的指针,迭代器和引用上,在函数参数和返回值上,在局部变量上,在成员函数上,const 是一个强有力的盟友。只要可能就用它,你会为你所做的感到高兴。

  Things to Remember

  ·将某些东西声明为 const 有助于编译器发现使用错误。const 能被用于对象的任何范围,用于函数参数和返回类型,用于整个成员函数。

  ·编译器坚持二进制位常量性,但是你应该用概念上的常量性(conceptual constness)来编程。(此处原文有误,conceptual constness 为作者在本书第二版中对 logical constness 的称呼,正文中的称呼改了,此处却没有改。其实此处还是作者新加的部分,却使用了旧的术语,怪!——译者)

  ·当 const 和 non-const 成员函数具有本质上相同的实现的时候,使用 non-const 版本调用 const 版本可以避免重复代码。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章