c++ 有const和没有const的相同函数- When and why?

i34xakig  于 2023-04-13  发布在  其他
关注(0)|答案(8)|浏览(198)
T& f() { // some code ... }
const T& f() const { // some code ... }

我已经看过几次了(在我目前学习的入门书中)。我知道第一个const使返回值成为const,换句话说:不可修改。第二个const允许函数也可以为const声明的变量调用,我相信。
但是为什么要在同一个类定义中同时使用这两个函数呢?编译器如何区分它们呢?我相信第二个f()(with const)也可以用于非常数变量。

mi7gmzs6

mi7gmzs61#

但是为什么要在同一个类定义中包含这两个函数呢?
拥有这两种功能,您可以:

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

如果只使用第一个,则不能在const对象上调用它;如果只使用第二个,则不能使用它来修改它返回引用的对象。
编译器如何区分它们?
当在const对象上调用函数时(或通过引用或指向const的指针),它选择const重载。否则,它选择另一个重载。
我相信第二个f()(带const)也可以被非const变量调用。
如果这是唯一的重载,那么它可以。对于这两个重载,将选择非const重载。

vyswwuz2

vyswwuz22#

  • 但为什么要在同一个类定义中包含这两个函数?*

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

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.
rqqzpn5f

rqqzpn5f3#

第一个不带const的类允许调用者修改对象,该对象通常是其方法被调用的类的成员。
第二个示例中,我们的宿主类处于只读模式,也允许对其成员进行只读访问。
默认情况下,如果constance规则允许,则调用非const版本。
最常见的例子之一是某种集合/数组类型的类。

class Array
{
   private:
      MyType members[MySize];

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

假设它们被实现了,并且可能是一个模板,或者它们是具体的类型和大小。我将演示const重载。
现在我们可以让某个人使用这个类了。你可能需要设置一个值。

Array myArray;
myArray[ 3 ] = myObject;

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

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

所以你可以看到我可以使用这种表示法来设置一个值和读取一个值。就像operator[]一样,你可以将这种技术应用于任何方法。

cuxqih21

cuxqih214#

在Qt框架中有一个很好的例子。
看一下QImage类。
有两个公共函数:

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

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

a0zr77ik

a0zr77ik5#

函数调用括号后的限定符适用于成员函数的隐藏参数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选择的方法是允许这些限定符在函数括号之外。
编译器区分这些函数的方式在各个方面都与常规函数重载相同,因为这正是它的本质,尽管语法很奇怪。
此外,const不是唯一的限定符。您还可以添加volatile限定符,并且在C
11中,您还可以添加左值和右值引用限定符。
我们需要这个函数的两个几乎相同的副本的原因是因为没有直接的方法来提取一个单一的差异:不同的返回类型。如果我们有一个const对象,并且该对象有一个getter,该getter返回对它所包含的内容的引用,该引用需要与整个对象相同。

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是const,我们不能返回一个非const引用。但是如果允许的话,这是错误的,因为我们不应该修改const对象的成员。所以Foo::get_i() const需要返回一个const引用到i

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

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

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

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

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_cast删除const只有在我们确定原始对象不是const时才是安全的。在这种情况下我们确实知道,因为我们知道我们首先将const添加到非const对象。

xwmevbvl

xwmevbvl6#

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

#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;

}
vof42yt1

vof42yt17#

如前所述,根据调用对象的常量,可以使用const和非const版本的函数。该范例经常与operator[]一起用于数组。避免代码重复的一种方法(摘自Scott Meyers的书Effective C++)是将const_cast函数返回到非常量重载中,例如:

// 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
}
luaexgnf

luaexgnf8#

如果你想让一个函数的const和non-const版本都有相同的实现,而不需要重复代码,你可以在non-const方法上使用const_cast。下面是一个例子:

struct MyStruct {
template<typename Func, typename... Args>
bool Foo(Func&& func, Args&&... args) {
    // Implementation for non-const version
    if (!func(1, std::forward<Args>(args)...)) { return false; }
    return true;
}

template<typename Func, typename... Args>
bool Foo(Func&& func, Args&&... args) const {
    // Call the non-const version of the function
    return const_cast<MyStruct*>(this)->Foo(std::forward<Func>(func), std::forward<Args>(args)...);
}

};

相关问题