具有 const 和不具有 const 的相同功能 - 何时以及为什么?

Same function with const and without - When and why?

T& f() { // some code ... }
const T& f() const { // some code ... }

我已经看过几次了(在我迄今为止一直在学习的介绍性书中)。我知道第一个 const 使 return 值成为 const,换句话说:不可修改。第二个 const 允许函数也可以为 const 声明的变量调用,我相信。

但是为什么要在同一个 class 定义中同时使用这两个函数?编译器如何区分这些?我相信第二个 f() (带有 const)也可以为非常量变量调用。

第一个没有 const 的允许调用者修改对象,该对象通常是 class 方法被调用的成员。

第二个,我们的主机 class 处于只读模式,也允许对其成员进行只读访问。

默认情况下,如果 constness 规则允许,则调用非 const 版本。

最常见的例子之一是某种集合/数组类型 class。

class Array
{
   private:
      MyType members[MySize];

   public:
      MyType & operator[]( size_t index );
      const MyType & operator[]( size_t index ) const;
};

假设它们已经实现并且它可能是一个模板或者它们是具体的类型和大小。我正在演示 const 重载。

现在我们可以让某人使用 class。您可能想要设置一个值。

Array myArray;
myArray[ 3 ] = myObject;

或者您可能只是在阅读它:

const Array& myArrayRef = getArrayRef(); // gets it to read
const MyType & myValueRef = myArrayRef[ 3 ];

所以你看我可以使用符号来设置一个值和读取一个值。 与 operator[] 一样,您可以将此技术应用于任何方法。

但是为什么你会在同一个 class 定义中同时拥有这两个函数?

有时您想为同一操作提供不同的语义,这取决于它是在 const 对象还是 non-const 对象上调用。我们以std::stringclass为例:-

char& operator[](int index);
const char& operator[](int index) const;

在这种情况下,当通过 const 对象调用 operator[] 时,您不会让用户更改字符串的内容。

const std::string str("Hello");
str[1] = 'A';     // You don't want this for const.

另一方面,如果是非常量字符串,您可以让用户更改字符串的内容。这就是不同重载的原因。

编译器是如何区分这些的?

编译器检查是否在 const 对象或 non-const 对象上调用了该方法,然后适当地调用该方法。

const std::string str("Hello");
cout << str[1];           // Invokes `const` version.

std::string str("Hello");
cout << str[1];           // Invokes non-const version.

But why would you have both functions in one and the same class definition?

两者兼具,您可以:

  • 在可变对象上调用函数,并根据需要修改结果;和
  • const 对象上调用函数,只看结果。

只有第一个,您不能在 const 对象上调用它。只有第二个,你不能用它来修改它 returns 引用的对象。

And how does the compiler distinguish between these?

当在 const 对象上(或通过指向 const 的引用或指针)调用函数时,它选择 const 重载。否则它会选择另一个重载。

I believe that the second f() (with const) can be called for non-const variables as well.

如果那是唯一的过载,那么它可以。对于这两个重载,将选择非 const 重载。

它允许您以只读方式访问 const 实例数据,同时仍然能够修改非 const 实例数据。

#include <iostream>

class test
{
  public:
    test() : data_(0) {}

    int& f() { return data_; }
    const int& f() const { return data_ }

  private:
    int data_;
};

int main(void)
{
  const test rock;
  test paper;

  /* we can print both */
  std::cout << rock.f() << std::endl;
  std::cout << paper.f() << std::endl;

  /* but we can modify only the non const one */
  // rock.f() = 21;
  paper.f() = 42;

}

函数调用括号后的限定符适用于成员函数的隐藏this参数:

成员函数void Foo::bar()有点像这样:void bar(Foo *this)。但是如果 Foo 对象是 const 会发生什么?

struct Foo {
    void bar();
};

const Foo f{};
f.bar();

嗯,由于Foo::bar()接受了一个Foo *this参数,不允许是const,所以上面的f.bar();编译失败。所以我们需要一种方法来限定隐藏的 this 参数,而 C++ 选择这样做的方式是允许这些限定符出现在函数 parens.

之外。

编译器区分这些函数的方式在各个方面都与常规函数重载相同,因为尽管语法很奇怪,但它确实是这样。

此外,const 不是唯一的限定词。您还可以添加 volatile 限定符,在 C++11 中,您还可以放置左值和右值引用限定符。


我们需要这个函数的两个几乎相同的副本的原因是因为没有直接的方法来找出唯一的区别:不同的 return 类型。如果我们有一个 const 对象,并且该对象有一个 getter,return 是对其包含的内容的引用,则该引用需要与整个对象一样被限定。

struct Foo {
  int i;
  int &get_i() const { return i; }
};

int main() {
  const Foo f{};
  f.get_i() = 10; // i should be const!
}

上面的代码甚至无法编译,因为在 Foo::get_i() const 中,i 是常量,我们不能 return 对它的非常量引用。但如果允许的话,那就错了,因为我们不应该能够修改 const 对象的成员。所以 Foo::get_i() const 需要 return 对 i.

的常量引用
int const &Foo::get_i() const { return i; }

但是我们应该能够修改非常量对象的成员,

int main() {
  Foo f{};
  f.get_i() = 10; // should be fine
}

所以我们不能只有这个功能。当 Foo 对象本身不是 const 时,我们需要一个 return 是非常量引用的函数。所以我们根据对象的const-ness重载函数:

struct Foo {
  int i;
  int const &get_i() const { return i; }
  int &get_i() { return i; }
};

如果函数体更复杂,有一种可能的选择可以避免重复:

struct Foo {
  int i;
  int const &get_i() const { return i; }

  int &get_i() { return const_cast<int &>(const_cast<Foo const *>(this)->get_i()); }
};

也就是说,非 const 重载将其实现委托给 const 重载,使用 const_cast 修复类型。添加 const 总是安全的。只有当我们确定原始对象不是 const 时,使用 const_cast 删除 const 才是安全的。我们确实知道在这种情况下,因为我们知道我们首先将 const 添加到非 const 对象。

Qt框架中有一个很好的例子。

看看 QImage class.

有两个public函数:

const uchar* scanLine (int i) const;
uchar* scanLine (int i);

第一个仅供读取访问。第二种是修改扫描线的情况

为什么这种区分很重要? 因为 Qt 使用 implicit data sharing。这意味着,如果您执行以下操作,QImage 不会立即执行深度复制:

QImage i1, i2;
i1.load("image.bmp");
i2 = i1;                        // i1 and i2 share data

相反,仅当您调用实际修改两个图像之一的函数(如非常量扫描线)时才复制数据。

如前所述,您可以使用 const 和非 const 版本的函数,具体取决于调用对象的常量性。该范例经常与 operator[] 一起用于数组。避免代码重复的一种方法(取自 Scott Meyers 的《Effective C++》一书)是 const_cast 非常量重载中的 const 函数 return,例如:

// returns the position of some internal char array in a class Foo
const char& Foo::operator[](std::size_t position) const
{
    return arr[position]; // getter 
}

// we now define the non-const in terms of the const version
char& Foo::operator[](std::size_t position) 
{
    return const_cast<char&>( // cast back to non-const
        static_cast<const Foo&>(*this)[position] // calls const overload
    ); // getter/setter
}