Peter Kasting | 5e04bd3 | 2019-05-21 21:44:10 | [diff] [blame] | 1 | # Using Const Correctly |
| 2 | |
| 3 | The **TLDR**, as stated in [C++ Dos and Don'ts](c++-dos-and-donts.md): |
| 4 | *** promo |
| 5 | For safety and simplicity, **don't return pointers or references to non-const |
| 6 | objects from const methods**. Within that constraint, **mark methods as const |
| 7 | where possible**. **Avoid `const_cast` to remove const**, except when |
| 8 | implementing non-const getters in terms of const getters. |
| 9 | *** |
| 10 | |
| 11 | ## A brief primer on const |
| 12 | |
| 13 | To the compiler, the `const` qualifier on a method refers to _physical |
| 14 | constness_: calling this method does not change the bits in this object. What |
| 15 | we want is _logical constness_, which is only partly overlapping: calling this |
| 16 | method does not affect the object in ways callers will notice, nor does it give |
| 17 | you a handle with the ability to do so. |
| 18 | |
| 19 | Mismatches between these concepts can occur in both directions. When something |
| 20 | is logically but not physically const, C++ provides the `mutable` keyword to |
| 21 | silence compiler complaints. This is valuable for e.g. cached calculations, |
| 22 | where the cache is an implementation detail callers do not care about. When |
| 23 | something is physically but not logically const, however, the compiler will |
| 24 | happily accept it, and there are no tools that will automatically save you. |
| 25 | This discrepancy usually involves pointers. For example, |
| 26 | |
| 27 | ```cpp |
| 28 | void T::Cleanup() const { delete pointer_member_; } |
| 29 | ``` |
| 30 | |
| 31 | Deleting a member is a change callers are likely to care about, so this is |
| 32 | probably not logically const. But because `delete` does not affect the pointer |
| 33 | itself, but only the memory it points to, this code is physically const, so it |
| 34 | will compile. |
| 35 | |
| 36 | Or, more subtly, consider this pseudocode from a node in a tree: |
| 37 | |
| 38 | ```cpp |
| 39 | class Node { |
| 40 | public: |
| 41 | void RemoveSelf() { parent_->RemoveChild(this); } |
| 42 | void RemoveChild(Node* node) { |
| 43 | if (node == left_child_) |
| 44 | left_child_ = nullptr; |
| 45 | else if (node == right_child_) |
| 46 | right_child_ = nullptr; |
| 47 | } |
| 48 | Node* left_child() const { return left_child_; } |
| 49 | Node* right_child() const { return right_child_; } |
| 50 | |
| 51 | private: |
| 52 | Node* parent_; |
| 53 | Node* left_child_; |
| 54 | Node* right_child_; |
| 55 | }; |
| 56 | ``` |
| 57 | |
| 58 | The `left_child()` and `right_child()` methods don't change anything about |
| 59 | `|this|`, so making them `const` seems fine. But they allow code like this: |
| 60 | |
| 61 | ```cpp |
| 62 | void SignatureAppearsHarmlessToCallers(const Node& node) { |
| 63 | node.left_child()->RemoveSelf(); |
| 64 | // Now |node| has no |left_child_|, despite having been passed in by const ref. |
| 65 | } |
| 66 | ``` |
| 67 | |
| 68 | The original class definition compiles, and looks locally fine, but it's a |
| 69 | timebomb: a const method returning a handle that can be used to change the |
| 70 | system in ways that affect the original object. Eventually, someone will |
| 71 | actually modify the object, potentially far away from where the handle is |
| 72 | obtained. |
| 73 | |
| 74 | These modifications can be difficult to spot in practice. As we see in the |
| 75 | previous example, splitting related concepts or state (like "a tree") across |
| 76 | several objects means a change to one object affects the behavior of others. |
| 77 | And if this tree is in turn referred to by yet more objects (e.g. the DOM of a |
| 78 | web page, which influences all sorts of other data structures relating to the |
| 79 | page), then small changes can have visible ripples across the entire system. In |
| 80 | a codebase as complex as Chromium, it can be almost impossible to reason about |
| 81 | what sorts of local changes could ultimately impact the behavior of distant |
| 82 | objects, and vice versa. |
| 83 | |
| 84 | "Logically const correct" code assures readers that const methods will not |
| 85 | change the system, directly or indirectly, nor allow callers to easily do so. |
| 86 | They make it easier to reason about large-scale behavior. But since the |
| 87 | compiler verifies physical constness, it will not guarantee that code is |
| 88 | actually logically const. Hence the recommendations here. |
| 89 | |
| 90 | ## Classes of const (in)correctness |
| 91 | |
| 92 | In a |
| 93 | [larger discussion of this issue](https://groups.google.com/a/chromium.org/d/topic/platform-architecture-dev/C2Szi07dyQo/discussion), |
| 94 | Matt Giuca |
| 95 | [postulated three classes of const(in)correctness](https://groups.google.com/a/chromium.org/d/msg/platform-architecture-dev/C2Szi07dyQo/lbHMUQHMAgAJ): |
| 96 | |
| 97 | * **Const correct:** All code marked "const" is logically const; all code that |
| 98 | is logically const is marked "const". |
| 99 | * **Const okay:** All code marked "const" is logically const, but not all code |
| 100 | that is logically const is marked "const". (Basically, if you see "const" you |
| 101 | can trust it, but sometimes it's missing.) |
| 102 | * **Const broken:** Some code marked "const" is not logically const. |
| 103 | |
| 104 | The Chromium codebase currently varies. A significant amount of Blink code is |
| 105 | "const broken". A great deal of Chromium code is "const okay". A minority of |
| 106 | code is "const correct". |
| 107 | |
| 108 | While "const correct" is ideal, it can take a great deal of work to achieve. |
| 109 | Const (in)correctness is viral, so fixing one API often requires a yak shave. |
| 110 | (On the plus side, this same property helps prevent regressions when people |
| 111 | actually use const objects to access the const APIs.) |
| 112 | |
| 113 | At the least, strive to convert code that is "const broken" to be "const okay". |
| 114 | A simple rule of thumb that will prevent most cases of "const brokenness" is for |
| 115 | const methods to never return pointers to non-const objects. This is overly |
| 116 | conservative, but less than you might think, due to how objects can transitively |
| 117 | affect distant, seemingly-unrelated parts of the system. The discussion thread |
| 118 | linked above has more detail, but in short, it's hard for readers and reviewers |
| 119 | to prove that returning pointers-to-non-const is truly safe, and will stay safe |
| 120 | through later refactorings and modifications. Following this rule is easier |
| 121 | than debating about whether individual cases are exceptions. |
| 122 | |
| 123 | One way to ensure code is "const okay" would be to never mark anything const. |
| 124 | This is suboptimal for the same reason we don't choose to "never write comments, |
| 125 | so they can never be wrong". Marking a method "const" provides the reader |
| 126 | useful information about the system behavior. Also, despite physical constness |
| 127 | being different than logical constness, using "const" correctly still does catch |
| 128 | certain classes of errors at compile time. Accordingly, the |
| 129 | [Google style guide requests the use of const where possible](http://google.github.io/styleguide/cppguide.html#Use_of_const), |
| 130 | so mark methods const when they are logically const. |
| 131 | |
| 132 | Making code more const correct leads to cases where duplicate const and non-const getters are required: |
| 133 | |
| 134 | ```cpp |
| 135 | const T* Foo::GetT() const { return t_; } |
| 136 | T* Foo::GetT() { return t_; } |
| 137 | ``` |
| 138 | |
| 139 | If the implementation of GetT() is complex, there's a |
| 140 | [trick to implement the non-const getter in terms of the const one](https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func/123995#123995), |
| 141 | courtesy of _Effective C++_: |
| 142 | |
| 143 | ```cpp |
Peter Kasting | d79e886 | 2021-04-13 23:34:47 | [diff] [blame] | 144 | T* Foo::GetT() { return const_cast<T*>(base::as_const(*this).GetT()); } |
Peter Kasting | 5e04bd3 | 2019-05-21 21:44:10 | [diff] [blame] | 145 | ``` |
| 146 | |
| 147 | While this is a mouthful, it does guarantee the implementations won't get out of |
| 148 | sync and no const-incorrectness will occur. And once you've seen it a few times, |
| 149 | it's a recognizable pattern. |
| 150 | |
| 151 | This is probably the only case where you should see `const_cast` used to remove |
| 152 | constness. Its use anywhere else is generally indicative of either "const |
| 153 | broken" code, or a boundary between "const correct" and "const okay" code that |
| 154 | could change to "const broken" at any future time without warning from the |
| 155 | compiler. Both cases should be fixed. |