4

As an embedded firmware developer, I often write classes to represent/act as drivers for hardware on a microcontroller. These driver classes will usually contain pointers to hardware registers and other state variables.

Often, these drivers inherit from some generic interface. For example, assume I have the following very basic interface for an ADC

class IADC
{
public:
    virtual uint32_t getVoltage() const = 0;
};

This interface simply states that a generic ADC driver will have a method which returns a voltage reading.

This is where my question comes in. My immediate thought is to mark a method like this as const because it is only reading a voltage value. Conceptually, reading a value does not change the state of a driver.

However, in practice, I have found that this assumption is not correct. In my ADC example, reading the voltage requires mutating some registers (to start the reading), changing the internal state machine, etc... Thus, the getVoltage() method cannot be marked as const.

So, should I not be marking my get methods in my interfaces as const? Or perhaps should I use some other feature like mutable? Do I have an incorrect interpretation of what const implies?

2
  • 2
    Excluding mock implementations for unit tests etc, how many implementations have you written of IADC which do not need to modify anything to implement getVoltage? Aug 23, 2023 at 14:19
  • 1
    @PhilipKendall Interesting that you mention this because, now that I think about it, the only implementation that did not need to modify anything was the mock for unit testing... Aug 23, 2023 at 14:28

4 Answers 4

8

Given your comment that

the only implementation that did not need to modify anything was the mock for unit testing

I'm going to argue that you are modelling the system at the wrong level. While at a high level you can view "acquire the current voltage" as a read-only operation, as you're discovering that isn't true at the lower level you're working at. I'd argue your operation is more like readVoltage() which needs to do a bunch of stuff rather than a simple atomic "get", at which point it becomes "acceptable" for it to be non-const.

2
  • 2
    I like the distinction you made between "getVoltage" and "readVoltage." That really helps me understand the distinction between the two implementation concepts. Aug 23, 2023 at 14:48
  • I would argue that get is the wrong prefix as it suggest hardly any work, not because it modifies internal state. The function could still be const iff it doesn't change the high-level state of the system. As an example for the latter, using internal locks is fine in just about all functions, as is caching a complex calculation. Aug 23, 2023 at 17:13
1

I think Phillip Kendall's answer is reasonable, but somewhat incomplete.

At least in my view, what you would typically want for a case like this is to separate the physical implementation from what it logically represents.

The physical implementation is the ADC itself. From the viewpoint of the ADC itself, reading the voltage does affect internal state, so the member function to get a voltage should not be const.

For the moment, let's assume what we're reading is the voltage from a thermocouple, so what we're really doing is reading a temperature. At that level of abstraction, we would normally expect that reading a temperature has no effect on the remainder of the system, so the code to retrieve that will be const. It will use (and probably own) an instance of ADC, which will probably be mutable, signaling that taking a reading can affect its state, but not the logical state of the system as a whole.

1

const is an interesting thing in interfaces, because it doesn't mean "at least readable". It means "not writable," which is inherently restricting.

While I'm a fan of using const as much as possible (better yet, having your language make const the default, and mutability be opt-in, rather than the other way around), I don't think it's a good default for interfaces, in general. It just restricts flexibility for implementing the interface, and doesn't give you much in exchange.

I would suggest adding const to interfaces only when it has a very direct benefit. It's hard to even think of an example, but one might be a Counter interface with increment() and peek() methods. Not mutating a counter during a peek() is pretty core to its correctness, and an implementation that doesn't do that would probably be quite surprising.

1

const in C++ has two meanings: For objects, it means they cannot be modified and any attempt to modify them invokes undefined behaviour. For data referenced by a const pointer or const reference, it means you are not allowed to modify the data through this pointer or reference. (But another thread or a function you call might change the data, so there is no guarantee it remains unchanged).

Then you have mutable class members: You are allowed to change mutable members of const instances. For example if an instance caches something, and you don’t consider loading from cache as “changing” the object. And finally you have const methods which you are allowed to invoke for const instances.

Now ask yourself: These operations that you perform, do you consider them to change the object? Say you write ten bytes to a serial port, is your serial port instance changed in your view? So the question is not “do I write to hardware registers” but “is the object before and after the call the same? For example, with the same serial port I would consider changing the Bitrate a change in the object.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.