std::list: iterator or const_iterator?

Started by
24 comments, last by cache_hit 14 years, 2 months ago
Quote:Original post by Sneftel
Also consider this:
*** Source Snippet Removed ***
Which is fine if we just want to inspect that relative. But what if we want to change it? We could make a non-const version of the code (requiring, BTW, the wholesale copy and paste and s/const//g of 18 auxiliary functions, representing 1300 lines of code, YIKES) but that doesn't feel right either, because all this searching in the family tree really SHOULD be guaranteed not to change anything.

What we really want is a way to talk about const-ness context for the family tree as a whole. Either we are a piece of code which is allowed to make babies and give them to people, or we are not. If we are, fine, all the code we need operates on const*s and returns const*s. If we are, however, all the constness in the world should not keep us from controlling Nodes that are under our control.


I've always preferred to think of const as meaning "not only will I not modify anything, I will not even do anything that makes it possible for someone else to modify it".

The problem with the Node example is that the term Node is being overloaded, because a Node really *is* a tree due to the recursive nature of the data structure. So, the second version of the Node data structure seems natural to me, although i admit it's frequently annoying to have to duplicate code in these types of functions.

I actually personally feel like const *should* be taken to mean that you're not allowed to change the state of things it knows about. I guess it depends on what you mean by "know about" though. If you want to make distinctions between statements like:

A : "I know about you, but I don't necessarily care about you as part of my state"
B : "I know about you, and you are an integral part of my behavior. A change to you is a change to me"

Then I think that's actually a legitimate use of the mutable keyword ('mutable' being used for situations of type A).


In your second example, I see again what you're saying but to me it's the same situation. C++'s definition of const means that not only will the function not change anything, it won't *expose* anything either. You shouldn't be able to get access to modify an object's state by calling operations you're allowed to perform if you're only allowed to perform read-only operations.


Take the OP's example (as much as we actually know about the example anyway) as a case in point. He said he's trying to make the same class operate with both Contours and ConstContours. Surely you should not be able to modify a ConstContour right? Otherwise what exactly *is* const about it?

When you really want to make the disctinction between A & B that I described above, I guess there's always the mutable keyword. Although I think I actually find a legitimate use for it less than once a year, I suppose it's possible this is one of those cases. I'd be interested to hear more about this Contour class though, and how he's using the same code to operate on const and non-const contours. I'm positive there's a solution involving some minor meta-programming, or a template specialization, or something that is type-safe as well as const correct, and still allows a list to be used (assuming that's even the best datastructure for the job).
Advertisement
Quote:In your second example, I see again what you're saying but to me it's the same situation. C++'s definition of const means that not only will the function not change anything, it won't *expose* anything either. You shouldn't be able to get access to modify an object's state by calling operations you're allowed to perform if you're only allowed to perform read-only operations.
Const correctness is only as useful as long as it finds bugs. If you're not allowed to make your 1300-line find_closest_male_descendant_named_Frances module const-correct (even though it absolutely should not be allowed to change the passed-in Node), simply because you plan to use the result later in a non-const context, you're not getting the actual benefits of const.
Quote:Original post by Sneftel
Quote:In your second example, I see again what you're saying but to me it's the same situation. C++'s definition of const means that not only will the function not change anything, it won't *expose* anything either. You shouldn't be able to get access to modify an object's state by calling operations you're allowed to perform if you're only allowed to perform read-only operations.
Const correctness is only as useful as long as it finds bugs. If you're not allowed to make your 1300-line find_closest_male_descendant_named_Frances module const-correct (even though it absolutely should not be allowed to change the passed-in Node), simply because you plan to use the result later in a non-const context, you're not getting the actual benefits of const.


I agree, except for that I think not having the function be const in the first place actually *is* const correct. The problem is just that people are used to visualizing "finds" as non-mutating operations, when really the function should be called "let_me_write_to_the_first_male_descendant_named_frances_if_such_a_descendant_exists".

In a good design, the whole find thing would ultimately just be a template function that returned an appropriate iterator type based on the argument type, so you wouldn't have the issue of having to maintain two versions of a 1300-line function and the compiler would even be smart enough to not generate additional code for the two versions.

But if a function's purpose is to expose write access to something, then by definition it shouldn't be const, and that is being const correct. The OP has a ConstContour, and to me that just screams out that he's doing something wrong if he's trying to find a way to modify it. Even if it's at a higher level, something is just wrong with that design.
Quote:Original post by cache_hit
But if a function's purpose is to expose write access to something, then by definition it shouldn't be const, and that is being const correct.

That may be a self-consistent definition of const correctness, but it is not a very useful definition of const correctness. It forces you to forego the sole benefit of const correctness in large swaths of your code. And it does so needlessly, because we have these tools to wipe away constness by guaranteeing our permission to write to an object's collection.
Quote:Original post by Sneftel
Quote:Original post by cache_hit
But if a function's purpose is to expose write access to something, then by definition it shouldn't be const, and that is being const correct.

That may be a self-consistent definition of const correctness, but it is not a very useful definition of const correctness. It forces you to forego the sole benefit of const correctness in large swaths of your code. And it does so needlessly, because we have these tools to wipe away constness by guaranteeing our permission to write to an object's collection.


Well I guess it's debatable whether or not it solves more problems than it causes. But it would be pretty scary if arbitrary code could gain write access to a data structure if they weren't explicitly given permission to write to it. Granted, they can already do that with const_cast, but at least with const_cast you can grep your codebase for it.

I think ultimately it just makes more sense to apply mutable in those cases though, as it requires no effort and is probably not even that bad a use of the keyword.
Quote:Original post by cache_hit
Well I guess it's debatable whether or not it solves more problems than it causes. But it would be pretty scary if arbitrary code could gain write access to a data structure if they weren't explicitly given permission to write to it.
That's the thing -- you can make it so that they can only gain write access if they WERE given permission. You can do it in a type-safe and const-safe manner, without a single const_cast. (There's a const_cast in the templated function I posted earlier in the thread, but it's adding constness, not removing it.)

Here's another example. Suppose I'm a MouseManager. I distribute mouse information by means of MouseRegion objects. A MouseRegion gives information on mouse events that occur while the pointer is in a given region. Various components have pointers to different MouseRegions, but a lot of them only have const pointers, because they aren't allowed to change the MouseRegion in question. (The accessors they use to get the events, of course, are all const member functions.) Now suppose that a particular Window which holds a MouseRegion const* decides to split off a child window, and split its MouseRegion as well. Of course it can't do that through the const*. However, I (MouseManager) want to expose this capability even without const access, so I have a special method to do it without granting non-const access:
MouseRegion const* MouseManager::SplitRegion(MouseRegion const* region, int split_loc)

How am I to implement SplitRegion? Even though I control all the regions, and have non-const access to them, I'm not allowed to touch that one, because Window isn't allowed to touch that one.
Thats a textbook (ignoring the fact that there actually *is* no real textbook for this sort of thing) use of const_cast. Const is really just trying to protect you from yourself. In this case, you have more information than the compiler. Its not unsafe at all to do the const_cast inside SplitRegion because presumably MouseManager is the only person that can create them.

Another solution would be to only expose const methods as part of MouseRegion's public interface and make MouseManager a friend of MouseRegion. Then you could return a non-const pointer to a MouseRegion
An interesting discussion. I am certainly familiar with the issue of a search type of function in your class that doesn't itself modify the object but needs to be non-const because it returns an iterator which some callers may use to modify the colection.
It seems clear to me that the problem is the tying together of the thing that gives you information about the location of the things searched for, with the notion of whether that thing should be modifyable. As the original poster states, certainly for the case of a vector, an integer does not forcibly tie these two things together, whereas a [const_]iterator does.

Templating the function based on the returned iterator type seems to be one solution, but it certainly feels like a mediocre solution at best. Provided one of the callers instantiates the const version then you can be assured that the searching function isn't inadvertently modifying the object's state.
"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms
I know I'm backtracking a bit, but I interpreted the problem to be something like this (probably because I've run into similar things before):

typedef std::list<Edge> Contour;
typedef std::vector<std::pair<Contour::iterator,Contour::iterator> > EdgePairs;

The dilemma being that if you have a const Contour, you can't add new elements to an EdgePairs from the contour because the former only accepts non-const iterators. On the other hand, if EdgePairs used const iterators, you wouldn't be able to modify any Edge in the Contour they belong to. In essence, by "fixing" the const-ness of the iterators, you're limiting what can be done when you have a Contour regardless of the contour's const-ness (a sort of const-coupling if you will). At the end of the day the only reason you're storing iterators is so that you can reference edges in the contour, but iterators force you to chose between const and non-cost versions even though the "true" const-ness of the reference changes depending on context; whether you have a const or non-cost Contour. Indexes into a vector are const-agnostic, which is why it worked perfectly before.
Quote:Original post by cache_hit
If you do have a const list, then use list::const_iterator. It seems like you feel like you have to make a global decision between *either* iterator *or* const_iterator. You can use both, if the list is const use const_iterator always. If the list is not const, use iterator if you're writing to the list, and const_iterator if you're reading from the list.


Ah yes indeed, I have to make that global decision if I want to keep the code simple at least. I have a struct representing an intersection between two edges of the polygon, and that struct contains the x/y coordinate of the intersection, as well as the index of the two edges that are involved in the intersection. There may be situations where I want to use this for const contours and others where I want to use it for non const ones.

Of course I can simply define two different structs, one for the const and one for the non const case, but it's just a bit more complex and even adds a bit of code duplication. And then I can't use the same instance both in a const and a non const function.

E.g. having a function taking a const contour that gives as output all the intersections, then I can't use that output in another function that would *modify* the contour based on these found intersections, unless the "find intersections" function would take a non-const contour as parameter instead, which is a shame because that function only reads from it...

And the question was, I was wondering, if a simple integer can be an index for both const and non const std::vectors, was there also a simple way in C++ to have something be a iterator for both const and non const std::list, but the answer turns out to be no...

This topic is closed to new replies.

Advertisement