Ch13.3: const Member Functions: Promising Not to Modify

English

提示:本页尚未翻译,当前显示英文原文。

1. What const After the Parameter List Means

Every member function receives the object it was called on as an invisible pointer, this (Ch13.7). Writing const after the parameter list changes the type of that pointer:


::std::int_least32_t value()   const noexcept;   // inside: this is counter const *
void                  add(::std::int_least32_t n) noexcept;   // inside: this is counter *

That is all it does, and it is enough to give you two guarantees at once:

2. The Rule: A Read-Only Member Function Is const

Ask one question about every member function you write: does it change the object? If the answer is no, the declaration ends with const. That is the rule, and it has no exceptions worth making.


#include <fast_io.h>
#include <cstdint>
using namespace ::fast_io::io;

class counter
{
public:
	void add(::std::int_least32_t n) noexcept
	{
		value_ += n;
	}
	::std::int_least32_t value() const noexcept
	{
		return value_;
	}

private:
	::std::int_least32_t value_{};
};

// A const reference can only call const member functions.
void show(counter const &c) throws
{
	println("value = ", c.value());
}

int main()
{
	try
	{
		counter c{};
		c.add(5);
		show(c);

		// A const reference to the same object: a read-only view of it.
		counter const &frozen{c};
		show(frozen);
		println("frozen.value() = ", frozen.value());
	}
	catch throws (::std::error e)
	{
		print("failed: ", e, "\n");
	}
}

value = 5
value = 5
frozen.value() = 5

show takes counter const & because it only reads. It calls c.value(), which compiles only because value() is const. Had value() been written without const, this perfectly ordinary function would not compile, and the fix would be on value(), not on show.

Here is what the compiler says when you try to change a const object — frozen.add(1):


error: passing 'const counter' as 'this' argument discards qualifiers [-fpermissive]
note: in call to 'void counter::add(int_least32_t)'

“discards qualifiers” means the call would throw away the const, and the compiler is not going to let you do that by accident.

Do not sprinkle const_cast to silence this. If you need to write inside a const member function, then either the function is not really read-only (remove its const, and accept that it cannot be called on a const object) or the object you were handed should not have been const in the first place. Both of those are information about your design; the cast throws the information away and leaves a program that lies.

3. Why This Is the Most Valuable Habit in the Chapter

const is contagious in the best way. Once the read-only member functions are marked, const & parameters appear everywhere, and every function in the program documents — and the compiler checks — who may change what. Three concrete consequences:

Two member functions, same name, differing only in const, are two different overloads. That is occasionally useful — a mutable accessor and a read-only one:

::std::int_least32_t &value() noexcept       { return value_; }
::std::int_least32_t  value() const noexcept { return value_; }
The compiler picks by the object: a mutable counter gets the first, a const one gets the second. Use this deliberately, not by accident: handing out a mutable reference to a private data member gives away the control you gained in Ch13.5.

4. const Member Functions and throws

The two specifications are independent and both appear at the end of the declaration, const first:


::std::int_least32_t value() const noexcept;   // read, cannot fail
void report() const throws;                    // read, may fail (it prints)

In the example above show is throws rather than noexcept because it prints, and its caller in main wraps the call in try { } catch throws (::std::error e). The class itself had to do nothing to make that work: a member function’s failure travels out of c.value(), then out of show, and is caught where the program is ready to deal with it (Ch3.16).