42

I've been teaching a C++ programming class for many years now and one of the trickiest things to explain to students is const overloading. I commonly use the example of a vector-like class and its operator[] function:

template <typename T> class Vector {
public:
    T& operator[] (size_t index);
    const T& operator[] (size_t index) const;
};

I have little to no trouble explaining why it is that two versions of the operator[] function are needed, but in trying to explain how to unify the two implementations together I often find myself wasting a lot of time with language arcana. The problem is that the only good, reliable way that I know how to implement one of these functions in terms of the other is with the const_cast/static_cast trick:

template <typename T> const T& Vector<T>::operator[] (size_t index) const {
     /* ... your implementation here ... */
}
template <typename T> T& Vector<T>::operator[] (size_t index) {
    return const_cast<T&>(static_cast<const Vector&>(*this)[index]);
}

The problem with this setup is that it's extremely tricky to explain and not at all intuitively obvious. When you explain it as "cast to const, then call the const version, then strip off constness" it's a little easier to understand, but the actual syntax is frightening,. Explaining what const_cast is, why it's appropriate here, and why it's almost universally inappropriate elsewhere usually takes me five to ten minutes of lecture time, and making sense of this whole expression often requires more effort than the difference between const T* and T* const. I feel that students need to know about const-overloading and how to do it without needlessly duplicating the code in the two functions, but this trick seems a bit excessive in an introductory C++ programming course.

My question is this - is there a simpler way to implement const-overloaded functions in terms of one another? Or is there a simpler way of explaining this existing trick to students?

the Tin Man
  • 150,910
  • 39
  • 198
  • 279
templatetypedef
  • 328,018
  • 92
  • 813
  • 992
  • 5
    +1 Nice question. My opinion is that don't teach them to implement one in terms of the other. It is a mere coincidence for this specific class that there is no difference between the implementations of both versions. – AraK Jan 04 '11 at 00:28
  • 6
    That is a good point, but very often const-overloaded functions really do the same thing. The pattern of "hand back a resource that's as const as you are" seems to come up a fair amount in the coding that I've done. – templatetypedef Jan 04 '11 at 00:30
  • 1
    My personal preference is that const is just far more work than it's genuinely worth. My experience is that I actually incredibly rarely actually declare variables that are const, and they're pretty much not complex types anyway. If I was teaching C++, I wouldn't inflict const on my poor students. – Puppy Jan 04 '11 at 00:44
  • Does anyone know a reason why the compiler couldn't generate const version from non-const one automatically when it is possible? – Roman L Jan 04 '11 at 00:45
  • @7vies: How the hell would the compiler know if it's possible or not? C++ is extremely anal about const, and doesn't trust it's compiler to perform many intelligent optimizations or other language improvements, so I find it very unlikely. – Puppy Jan 04 '11 at 00:47
  • 7
    @DeadMG- The problem with ditching const is that it comes up in a few key places, like pass-by-const-reference or the parameters to the copy constructor or assignment operator. I'd feel like I was doing a disservice to students by not letting them know about const. – templatetypedef Jan 04 '11 at 00:47
  • 1
    @DeadMG, very simply: try to add `const`, and if it does not compile - it is not const. What would be wrong with that? upd: Oups, I got it myself, it's not that straightforward as the return type might be different etc – Roman L Jan 04 '11 at 00:48
  • @7vies: What if I don't want a const version? Why, the compiler might have to actually be smart about it, in which case none of this would even be necessary in the first place. @templatetypedef: I admit that if you don't have an rvalue reference compiler, there are some language situations where it's fairly unavoidable. However, in that case, I'd just teach them to cast away the constness. Many languages don't have const, and for good reason, imo. – Puppy Jan 04 '11 at 00:51
  • 2
    possible duplicate of [How do I remove code duplication between similar const and non-const member functions?](http://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-funct) – Steve Jessop Jan 04 '11 at 01:06
  • 3
    @DeadMG: `const` improves code readability, and reduces surprises. It's worth it. – Inverse Jan 04 '11 at 02:46
  • @Steve Jessop- I don't think this is a duplicate, since the question isn't "how do I do this?" (that's static_cast/const_cast) but "what's the best way of doing this in a way that's teachable?" – templatetypedef Jan 04 '11 at 02:48
  • 1
    @Inverse: You mean, it increases code readability by adding huge quantities of totally redundant language syntax and semantics for an incredibly small minority of cases? Interesting opinion. – Puppy Jan 04 '11 at 18:32
  • A macro for the function body would pass my CR much easier than a `const_cast<>`. – lorro Aug 30 '16 at 19:08
  • @templatetypedef A small side question: is there any particular reason you use static_cast to convert the non-const input to a const one, as opposed to using const_cast in this situation too? In particular, doing `return const_cast(const_cast(*this)[index]);`? – gowrath Apr 06 '17 at 02:14
  • That... is a really good question. This was adapted from Effective C++ many years back. I think you have a very good point! – templatetypedef Apr 06 '17 at 06:07
  • @gowrath could be because, in the general case, `const_cast` is likely to be more "dangerous" and sets off red flags. It's natural to use the "less dangerous" cast unless absolutely necessary. – Ayxan Haqverdili Feb 23 '21 at 18:39

7 Answers7

12

I usually consider this to be a language restriction, and advise people that -- unless they really know what they're doing -- they should just reimplement. In the vast majority of cases, these functions are simple one-line getters, so there's no pain.

In your capacity of teaching C++ I would feel even more strongly about this approach.

Lightness Races in Orbit
  • 358,771
  • 68
  • 593
  • 989
8

How about simply breaking it down into smaller steps?

const Vector<T>& const_this = *this;
const T& const_elem = const_this[index];
T& mutable_elem = const_cast<T&>(const_elem);
return mutable_elem;

You can even eliminate the static_cast this way, although you could leave it in if you think it would be clearer.

Sean
  • 27,102
  • 4
  • 72
  • 98
  • 1
    TG: This is not undefined behavior. The OP's code is essentially identical to that in Item 3 of Scott Meyer's "Effective C++," and my code is, as advertised, the same thing but in more steps. Kindly remove your downvote when you've convinced yourself of your error. – Sean Jan 04 '11 at 02:56
  • 2
    This would only be undefined behavior if we took an object that was initially const and tried stripping off constness. In this case, since you're calling a non-const member, you can be sure that the original object isn't const. Hence the Meyers trick. – templatetypedef Jan 04 '11 at 07:12
  • The "it" in my "How about breaking it down" was the OP's non-const operator[]. I thought that was obvious. – Sean Jan 07 '11 at 16:58
  • 1
    @LightnessRacesinOrbit You are just everywhere! xD – Des1gnWizard Jan 02 '16 at 13:19
  • 1
    @Des1gnWizard: Top 0.02% baby! – Lightness Races in Orbit Jan 02 '16 at 15:17
7

It is quite a weird option, but this can be done using a static template helper like this

// template parameters can be const or non-const
template<class Ret, class C>
static Ret& get(C* p, size_t index) { /* common code here like p->array[index] */ }

Then you can write

const T& operator[](size_t index) const { return get<const T>(this, index); }
T& operator[](size_t index) { return get<T>(this, index); }

This trick avoids casts (!) and double implementation, but, again, it looks weird to me :)

And a small remark about your snippet code, won't const_cast be enough instead of that static_cast, or am I missing something?

Roman L
  • 2,896
  • 22
  • 37
  • The static_cast is necessary so that the seemingly recursive call to operator[] actually is calling the const version. – templatetypedef Jan 04 '11 at 02:22
  • @templatetypedef: what I mean is `const_cast` in place of `static_cast`, what's wrong with that? – Roman L Jan 04 '11 at 02:32
  • I must be confused about what you're talking about. The logic here is that in the non-const version, I want to call the const version. To do so, I use static_cast to add const to the this pointer, making it a const Vector. Invoking operator[] then calls const T& operator[] (size_t) const. – templatetypedef Jan 04 '11 at 02:32
  • Ah, I see what you mean. I was under the impression that const_cast can only be used to strip constness from a pointer/reference, never to add it. And it looks like I was wrong about that! So thanks for pointing this out. – templatetypedef Jan 04 '11 at 02:34
  • +1 I've used this for some non-completely-trivial methods in the past (I had to use traits to derive intermediate types as I was using IBM's xlC++ on AIX back then and it was still a little bit template-challenged). – sehe Sep 22 '11 at 09:45
6

You can remove one cast by using a private method:
It adds a method but makes the casting less complex:

template <typename T>
class Vector
{
  public:
    T const& operator[](size_t i) const { return getValue(i);}
    T&       operator[](size_t i)       { return const_cast<T&>(getValue(i));}

  private:
    T const& getValue(size_t i) const   { return /* STUFF */;}
};
Martin York
  • 234,851
  • 74
  • 306
  • 532
  • How is this better? If the only objective is to remove one of the casts, could `auto const& p = *this; return const_cast(p[i]);` – Ayxan Haqverdili Feb 23 '21 at 18:41
  • I am not recommending implementing it twice. I feel like leaving it in the const version and calling that from the non-const version is the best option. Adding a helper method doesn't seem to add much value. – Ayxan Haqverdili Feb 23 '21 at 19:19
  • 1
    @AyxanHaqverdili Ahhh. I see now. Sure that is an alternative. I have no objects to this as alternative. – Martin York Feb 23 '21 at 19:24
3

In my opinion this is just silly. You're forcing one to be implemented in terms of the other simply for the sake of doing so, not because the resulting code is easier to maintain or understand. The reason your students are confused is probably because they SHOULD be.

Not every principle should be taken to the exclusive extreme. Sometimes being redundant is simply better.

Edward Strange
  • 38,861
  • 7
  • 65
  • 123
  • 1
    Because it is easier to teach principles with easy redundant examples. Once they understand the principle on an easy situation then it makes doing in a hrder situation easier to understand. – Martin York Jan 04 '11 at 01:27
  • 1
    @Martin - It's not easy to understand a situation if you can't understand why someone's created it. "Why is the instructor doing all this crap when I could just write 'return buffer[i]'?" Especially since there are a lot of instructors teaching a lot of total crap (dunno if this one is) they're just not going to listen if they don't get the point. A better example to teach implementing one operator in terms of another is something like operator +. – Edward Strange Jan 04 '11 at 16:55
  • This way you have to maintain one function, not two. Also, it is very clear that they do the same thing. – Ayxan Haqverdili Feb 23 '21 at 18:30
2

Instead of making one version call the other, you could let both call a helper function which finds the right item. You already seem to be introducing templates, so letting the helper function be a template as well should work and avoid the code duplication, and work for both const and non-const without any const_casts.

Alternately, you could use local variables to help break the expression into manageable pieces (with comments for each).

const ClassType& cthis = *this; // look, no explicit cast needed here :)
const T& elem = cthis[index]; // delegate to const version
return const_cast<T&>(elem); // ok to strip off the const, since we added it in the first place
Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
2

If the implementation will the exact same code (for this 'vector' class example or whatever), then why not have the non-const version call the const version, rather than the other way around. If for some reason the code has to modify a member, then maybe there shouldn't really be a const version (ignoring the whole mutable thing...).

Ivan K
  • 21
  • 1
  • 3
    It's generally a bad idea to have a const version call the non-const version because it subverts the safety checks you get by having a const function. In particular, if the non-const version does make a change for whatever reason (erroneous or necessary), having the const version call into it by stripping off constness might result in a const function modifying the receiver. Worse, if the object was declared const to begin with, this can have undefined behavior. But you're right - in some cases it might be a bad idea to have a const function in the first place. – templatetypedef Jan 04 '11 at 00:37
  • 3
    This is an interesting point. If one is semantically different from the other, then they probably should not have the same name. – Lightness Races in Orbit Jan 04 '11 at 00:40