How to compose mutable objects const correctly

248 views Asked by At

const is required if you try to pass a reference to a temporary object:

http://msdn.microsoft.com/query/dev12.query?appId=Dev12IDEF1&l=EN-US&k=k%28C4239%29;k%28vs.output%29&rd=true

Which means it's not possible in C++ to model a modifiable wrapper of an object as a temporary (without being forced to make the object an lvalue):

inline char * PcToUnix(AutoCStringBufferA & buffer) { return PcToUnix(buffer, buffer.size()); }
inline char * PcToUnix(CStringA & str) { return PcToUnix(make_autobuffer(str)); }

PcToUnix : converts a character buffer from CR+LF to CR in-situ. make_autobuffer : takes a CString, and locks its underlying character buffer so that we have direct access to manipulate it over the lifespan of the autobuffer.

So I cannot write a call statement which takes the underlying string, wraps it in a buffer management object (mutable), passes that on to a function that manipulates the contents of the buffer, and returns, unless the autobuffer is declared to be passed as const &:

WRONG:

inline char * PcToUnix(AutoCStringBufferA & buffer) { return PcToUnix(buffer, buffer.size()); }

OKAY?!

inline char * PcToUnix(const AutoCStringBufferA & buffer) { return PcToUnix(buffer, buffer.size()); }

But the "correct" form seems to be specifying a contract of: "I will not modify your buffer". The constness here is logically correct at the level of the autobuffer object - it is not modified for the duration of PcToUnix() - the buffer wrapper object itself (i.e. the autobuffer) is not modified during PcToUnix - so it's true enough that it is const...

But, but, it is modifying its underlying object - by definition - a wrapper of something else - and that something else IS MODIFIED.

This seems to me to be a fundamental flaw with C++ const. There is just no way to both satisfy the current rules (without either using non-standard compiler behavior or writing misleading API contracts or writing unnecessarily verbose code.

The verbose method:

inline char * PcToUnix(CStringA & str) { auto adapter = make_autobuffer(str); return PcToUnix(adapter); }

The misleading API method:

inline char * PcToUnix(const AutoCStringBufferA & buffer) { return PcToUnix(buffer, buffer.size()); }
inline char * PcToUnix(CStringA & str) { return PcToUnix(make_autobuffer(str)); }

If C++ offered a way to specify the constness of the wrapper as separate from the constness of the wrapped object, then we'd have the makings of a sane way to create rules and express the API succinctly. But I just don't see how to accomplish that.

Pre-smart-pointers (or for all contexts which do not have a wrapper layer) one can specify the constness of the pointer separately from the constness of the underlying object:

T * const pConstPointerToMutableObject;
const T * pMutablePointerToConstObject;
const T * const pConstPointerToConstObject;

But there isn't any such corresponding support for wrapper objects. There is no way to express the constness of the referer separately from the constness of the referee. Nor any set of reasonable rules for defining or controlling the commutative/associative/distributive nature of const.

I don't understand why this isn't brought up constantly. I stumble on this constantly, and find const to be an enormous PIA because of it.

Ideally, code should be trivially compostable - i.e. one should be able to wrapper some simpler object in another layer which adapts it in some way - maybe filters access, or adds smarts about managing subobjects, or delayed instantiation of the underlying thing, etc., so that complex and specific interfaces to ones data can be built up without having to redesign the basic object over and over - just wrap in in a layer or two that adds the necessary intelligence / logic / interface / adaptation to the context in which it is needed.

But C++ make this nigh-impossible due to const-correctness issues. (Or, I'm just too stupid to figure out how to do all of this, and in my extensive readings, I've managed to miss how it is to be done, or even that anyone is discussing these issues w/o just glossing over and ignoring this aspect of const). Perhaps someone here on SO can disabuse me of whatever my mistakes are??

1

There are 1 answers

8
justin On BEST ANSWER

The easy approach to const correctness: Use transitive const.

It seems you are asking for a little too much for free at the language level. It's actually designed (among other things) so that you pay for what you use, and to be safe and performant. And yes, it can be very verbose.

As far as containers which are physically-const-aware, C++ is certainly capable of supporting the distinction; e.g.:

void A(const std::shared_ptr<const bool>& p) { /* ... */}
void B(const std::shared_ptr<bool>& p) { /* ... */}

void C() {
 A(std::make_shared<bool>(false)); // << ok
 A(std::make_shared<const bool>(false)); // << ok

 B(std::make_shared<bool>(false)); // << ok
 B(std::make_shared<const bool>(false)); // << error. API forbids removal of const.
}

But the C++ standard library does not provide all the containers you might need in order to accomplish what you are asking for.

Enumerate the smart pointer variants you would need, and consider that they are responsible for object lifetime and conversion from one container to another. Also note how much shorter that list is if you support transitive const only. Then consider the APIs in the OP would generally be converted to templates to easily support the variations.

Sometimes, you have to deal with types which have less than ideal interfaces. In those cases it's often easiest to make a container for them, and to introduce your ideal form of const.

Using this approach, the containers do the majority of the heavy lifting (=saves you from that verbosity at the call-site).

Of course, the C++-looking way to do a string conversion would look more like:

std::string PCToUnix(std::string pString) {
 ...mutate pString...
 return pString;
}

Or in place:

void PCToUnix(std::string& pString) {
 ...mutate pString...
}