views:

383

answers:

8

I generally prefer constness, but recently came across a conundrum with const iterators that shakes my const attitude annoys me about them:

MyList::const_iterator find( const MyList & list, int identifier )
{
    // do some stuff to find identifier
    return retConstItor; // has to be const_iterator because list is const
}

The idea that I'm trying to express here, of course, is that the passed in list cannot/willnot be changed, but once I make the list reference const I then have to use 'const_iterator's which then prevent me from doing anything with modifing the result (which makes sense).

Is the solution, then, to give up on making the passed in container reference const, or am I missing another possibility?

This has always been my secret reservation about const: That even if you use it correctly, it can create issues that it shouldn't where there is no good/clean solution, though I recognize that this is more specifically an issue between const and the iterator concept.

Edit: I am very aware of why you cannot and should not return a non-const iterator for a const container. My issue is that while I want a compile-time check for my container which is passed in by reference, I still want to find a way to pass back the position of something, and use it to modify the non-const version of the list. As mentioned in one of the answers it's possible to extract this concept of position via "advance", but messy/inefficient.

A: 

If you're going to return a non-const accessor to the container, make the function non-const as well. You're admitting the possibility of the container being changed by a side effect.

This is a good reason the standard algorithms take iterators rather than containers, so they can avoid this problem.

Mark Ransom
But that negates the purpose of const in the first place. I want to say that the function will *not* change the list by side effect. Its the return value that might.. I understand that to create a non-const return iterator, you need a non-const list, but that's what I'm trying to get around. Really what I would want would be to use a non-const version of the list to convert the const_iterator to a normal iterator once returnedAlso I'm not sure how having an algorithm take in the iterator helps. If you want to guarantee that the function does not modify by side effect, how does that help you?
Catskul
"I want to say that the function will not change the list by side effect. Its the return value that might.." Like I said, a return value still falls under the function. If the return value lets it change, the function let's it change.
GMan
+1  A: 

Rather than trying to guarantee that the list won't be changed using the const keyword, it is better in this case to guarantee it using a postcondition. In other words, tell the user via comments that the list won't be changed.

Even better would be using a template that could be instantiated for iterators or const_iterators:

template <typename II> // II models InputIterator
II find(II first, int identifier) {
  // do stuff
  return iterator;
}

Of course, if you're going to go to that much trouble, you might as well expose the iterators of MyList to the user and use std::find.

Mark Ruzon
"better to guarantee it using a postcondition"... well that's no guarantee at all. Comments can get out of sync with code. Const is meant to *be* the guarantee. If I use two versions (via template or otherwise), then I might as well just use a non-const version and ignore the const one.Find was meant only to be an example btw.
Catskul
std::find works exactly in the manner I described. And a programmer who will alter the postcondition without making the proper change to the documentation is the same programmer who will remove const from your function.
Mark Ruzon
You could use that argument for other types of typesafety as well, but const as long with the other variants of typesafety are there to avoid trusting the coder/documentation. You are correct that the reason find offers two versions is the same as my issue, but if it were otherwise possible/easy to use/efficent, having only the const version would be preferable in the case of find as well.
Catskul
+1 since a postcondition in the comments is about all you can practically do in C++ -- but it's not the ideal solution. Catskul's last comment is illuminating.
j_random_hacker
I took the question as asking for a practical solution, not an ideal one. With pointers I can write const char* p to make the object p points to a constant, or I can write char* const p to make the pointer constant, or I can write const char* const p to make both the pointer and the object constant. The problem (one of many) with C++ is that there is no way to extend this notion to iterators or other views into the data. Fixing that would be ideal.
Mark Ruzon
+1  A: 

If you're changing the data directed by the iterator, you're changing the list.

The idea that I'm trying to express here, of course, is that the passed in list cannot/willnot be changed, but once I make the list reference const I then have to use 'cons_iterator's which then prevent me from doing anything with the result.

What is "dong anything"? Modifying the data? That's changing the list, which is contradictory to your original intentions. If a list is const, it (and "it" includes its data) is constant.

If your function were to return a non-const iterator, it would create a way of modifying the list, hence the list wouldn't be const.

GMan
see here: http://stackoverflow.com/questions/1557352/how-do-i-escape-the-constiterator-trap-when-passing-a-const-container-reference/1557472#1557472 to understand what I'm trying to say.
Catskul
I understand what you're saying. It's just not the C++ way. Either something is mutable or its not. If something can't mutate something, then it shouldn't somehow give someone that asks it to the ability to mutate. Just because the body of our function doesn't modify the list doesn't make the function const with respect to the list. If the function provides the ability to mutate the list, then the function mutates the list.
GMan
The crux of the issue is that in a manner of thinking iterators could just be indicators of position, and its the container that is mutable or immutable. If thought of as indicators of position, one that points to a const container or non const container would be equivalent. Since they also offer access to the list itself it gets more complicated. What I wanted was a way to use const to guarantee that the function doesn't modify anything, *and* return a position indicator. It's possible to extract this concept via advance, but messy. Using const to make a guarantee like this is the C++ way.
Catskul
+8  A: 

If I understand what you're saying correctly, you're trying to use const to indicate to the caller that your function will not modify the collection, but you want the caller (who may have a non-const reference to the collection) to be able to modify the collection using the iterator you return. If so, I don't think there's a clean solution for that, unless the container provides a mechanism for turning a const interator into a non-const one (I'm unaware of a container that does this). Your best bet is probably to have your function take a non-const reference. You may also be able to have 2 overloads of your function, one const and one non-const, so that in the case of a caller who has only a const reference, they will still be able to use your function.

rmeador
Yes you've understood me perfectly.
Catskul
Otherwise known as make a mutable and constant version of the functions, for mutable and constant access.
GMan
+2  A: 

Although I think your design is a little confusing (as others have pointed iterators allow changes in the container, so I don't see your function really as const), there's a way to get an iterator out of a const_iterator. The efficiency depends on the kind of iterators.


#include <list>

int main()
{
  typedef std::list<int> IntList;
  typedef IntList::iterator Iter;
  typedef IntList::const_iterator ConstIter;

  IntList theList;
  ConstIter cit = function_returning_const_iter(theList);

  //Make non-const iter point to the same as the const iter.
  Iter it(theList.begin());
  std::advance(it, std::distance<ConstIter>(it, cit));

  return 0;
}
ltcmelo
Yes, this is the sort of think I was looking for. It's unfortunate that it's so crufty/not guaranteed to be O(1). In a perfect world it would be: MyList::iterator itor = myNonConstList.nonConstItorFromConst( someConstItor ) and O(1)
Catskul
Just make the function take a non-const list, since you're going to be modifying it...
GMan
+5  A: 

It's not a trap; it's a feature. (:-)

In general, you can't return a non-const "handle" to your data from a const method. For example, the following code is illegal.

class Foo
   {
      public:
         int& getX() const {return x;}
      private:
         int x;
   };

If it were legal, then you could do something like this....

   int main()
   {
      const Foo f;
      f.getX() = 3; // changed value of const object
   }

The designers of STL followed this convention with const-iterators.


In your case, what the const would buy you is the ability to call it on const collections. In which case, you wouldn't want the iterator returned to be modifiable. But you do want to allow it to be modifiable if the collection is non-const. So, you may want two interfaces:

MyList::const_iterator find( const MyList & list, int identifier )
{
    // do some stuff to find identifier
    return retConstItor; // has to be const_iterator because list is const
}

MyList::iterator find( MyList & list, int identifier )
{
    // do some stuff to find identifier
    return retItor; 
}

Or, you can do it all with one template function

template<typename T>    
T find(T start, T end, int identifier);

Then it will return a non-const iterator if the input iterators are non-const, and a const_iterator if they are const.

JohnMcG
That certainly provides for using the function with const containers, but then I lose the guarantee that the non-const function does not modify the list. What I really would like is to use the const ness of the passed in reference to guarantee that the list is not modified int the body of the function. The real solution is to convert the const_iterator returned to a non-const via "advance" as per one of the other answers, but unfortunately it's unwieldy : /
Catskul
Catskul, if the function is going to return something that allows others to modify the list, then the function also needs permission to modify the list. The function cannot grant permissions it doesn't have. You're just going to have to accept that the function is *allowed* to modify the list, even though it doesn't exercise that right.
Rob Kennedy
The example you posted of class Foo is **not** correct. That should not compile in a standard conformant compiler (try comeau online for example). I assume you're talking about logical X physical constness. However, that's not the case with the reference in that example. A pointer would fit better to illustrate the problem.
ltcmelo
You're right -- I seem to remember a Meyers example similar to this. I'll check it out and edit.
JohnMcG
Thinking about it some more, I think the Meyers example was a public method returning a reference to a private variable.
JohnMcG
+2  A: 

What I've done with wrapping standard algorithms, is have a metaobject for determining the type of container:

namespace detail
{
    template <class Range>
    struct Iterator
    {
        typedef typename Range::iterator type;
    };

    template <class Range>
    struct Iterator<const Range>
    {
        typedef typename Range::const_iterator type;
    };
}

This allows to provide a single implementation, e.g of find:

template <class Range, class Type>
typename detail::Iterator<Range>::type find(Range& range, const Type& value)
{
    return std::find(range.begin(), range.end(), value);
}

However, this doesn't allow calling this with temporaries (I suppose I can live with it).

In any case, to return a modifiable reference to the container, apparently you can't make any guarantees what your function does or doesn't do with the container. So this noble principle indeed breaks down: don't get dogmatic about it.

I suppose const correctness is more of a service for the caller of your functions, rather that some baby-sitting measure that is supposed to make sure you get your simple find function right.


Another question is: how would you feel if I defined a following predicate and then abused the standard find_if algorithm to increment all the values up to the first value >= 3:

bool inc_if_less_than_3(int& a)
{
    return a++ < 3;
}

(GCC doesn't stop me, but I couldn't tell if there's some undefined behaviour involved pedantically speaking.)

1) The container belongs to the user. Since allowing modification through the predicate in no way harms the algorithm, it should be up to the caller to decide how they use it.

2) This is hideous!!! Better implement find_if like this, to avoid this nightmare (best thing to do, since, apparently, you can't choose whether the iterator is const or not):

template <class Iter, class Pred>
Iter my_find_if(Iter first, Iter last, Pred fun)
{
    while (first != last 
       && !fun( const_cast<const typename std::iterator_traits<Iter>::value_type &>(*first)))
        ++first;
    return first;
}
UncleBens
*"I suppose const correctness is more of a service for the caller of your functions, rather [than a baby sitter]..."* But that argument becomes recursive and then there would be no point to const in the first place. It sort of *is* there to baby sit; it's type safety. That way, for instance, you *should* be able to design functions that take a predicate and not worry that the predicate might modify its arguments. There is a conflict here not with const, but rather with iterators in that sometimes they are used as simple position indicators, but they are always more than that (when non const).
Catskul
Well, may-be that is somewhat misleading, but you must take into account the bigger picture. GMan makes an excellent point: if it was possible to take a const container, and return a non-const iterator, it would *break* const-correctness for the user. For this reason, you'll notice there are plenty of places in C++ where you need to provide both const and mutable overloads. In such cases, it seems best you can do is to implement both in terms of same code: you'll discover unwanted modifications when testing with const parameter.
UncleBens
+1, excellent post. (It's unfortunate that C++ is not more orthogonal so that we could forget about binding to temporaries being a special case.) The basis of the problem is that `const` is not granular enough for what Catskul wants -- it's an all-or-nothing proposition. But as you demonstrate with your mutating-predicate example, it's hard to imagine a more granular form of `const` that doesn't allow all kinds of "surprising" behaviour. IOW it's hard to imagine a more-granular form of `const` that is practically useful.
j_random_hacker
+1  A: 

You are thinking about your design in the wrong way. Don't use const arguments to indicate what the function does - use them to describe the argument. In this case, it doesn't matter that find() doesn't change the list. What matters is that find() is intended to be used with modifiable lists.

If you want your find() function to return a non-const iterator, then it enables the caller to modify the container. It would be wrong for it to accept a const container, because that would provide the caller with a hidden way of removing the const-ness of the container.

Consider:

// Your function
MyList::iterator find(const MyList& list, int identifier);

// Caller has a const list.
const MyList list = ...
// but your function lets them modify it.
*( find(list,3) ) = 5;

So, your function should take a non-const argument:

MyList::iterator find(MyList& list, int identifier);

Now, when the caller tries to use your function to modify her const list, she'll get a compilation error. That's a much better outcome.

alex tingle
(First let me say, that I dont want to remove the constness of the itor. I'm aware of how wrong that would be)*"Don't use const arguments to indicate what the function does - use them to describe the argument."* I was thinking that maybe I should think about it this way, but thinking again, it doesn't seem to make sense. That argument basically breaks down to *"only use const to satisfy the caller's need for const"* but that's clearly recursive and when you finally get to the top, the reason for using const is to guarantee that the code does not change the parameter which is exactly my goal.
Catskul
alex tingle
In a perfect world, functions which do not modify the referenced passed parameters would only offer a const form because that is the goal of const. It can't here obviously because iterators offer the ability to change the list.Because I only care about the position indicator-ness of the iterator, I consider the non-const form is a work around. The other work around that's uglier, but closer to the concept is "Advance"
Catskul