EvanED, Yakk and jareds wrote:Let's all hammer on Julian!
No, just kidding.
I know you guys are serious, and I realize I've been wrong about quite a few things.
I feel I shouldn't hijack this thread any longer with enormous quote trees, so I'll just concede the main points without properly attributing the posters who convinced me or the sources they referred to:
- Incrementing an iterator beyond past-the-end is always undefined behaviour, regardless of its state of mutability, and therefore it's never memory-safe. (OK, some incomplete attribution. @jareds: I had to read those ISO C++03 standard quotes a few times before I could understand how you derived your conclusion. Nice bit of induction you did there.)
- Despite the guarantees offered by STL containers and by STL algorithms, using iterators is never strictly guaranteed to be safe because the library user can make mistakes.
- Modifying an object inside a function to which it was passed by const reference is not undefined behaviour (I did a search of my own to confirm this; see here).
- That an argument to a function is passed by const reference of itself doesn't help the compiler to optimize the function.
Then, a few reactions to some less-central points that passed along the way.
@EvanED: I claimed Haskell and OCaml don't offer the possibility to define a non-generic function, and you rightly pointed out that they can. Still, my underlying claim remains valid that because of static typing, C++ and ML essentially behave the same with regard to generics and type inference: either the function is generic, or the type of its arguments can't be inferred from the calling context.
On treating someone like they don't know what they're talking about and hypocrisy:
@jareds: you're absolutely right, and in fact I have that pet peeve too.
@EvanED: I apologize for my way of responding to you. FWIW, I was really thinking that you
did know what you were talking about, even though I thought you were wrong about some things.
@jareds: of course STL algorithms don't guarantee to do the right thing if you pass them non-matching iterators. I admit I was sloppy in my formulation, but wasn't it obvious that I meant that STL algorithms guarantee to do the right thing if you pass them iterators that are valid
relative to each other?
On the possibility of practical immutable "functional" containers in C++:
While I agree with Yakk that it can be implemented, I think EvanED is right that it couldn't be done without violating the STL container interface conventions.
Finally, let me remark that I've learned something. Thanks to all three of you for the instructive comments.
Now that I've mostly cleaned my hands (I hope), I would like to return to the issue of the unexpected side effect because I think there is more to it. To summarize, we have (in C++)
- two objects of the same type T: arg and imp;
- a function frob, which takes arg as a const reference and modifies imp as a side effect.
If
arg and
imp happen to be the same object,
frob will behave in a way that programmers normally don't expect (this scenario was originally brought forward by EvanED to illustrate why constness in C++ isn't the same as immutability in Rust, but I won't dispute that anymore).
To make such a situation as plausible as possible, we assume that
imp is not an argument to
frob, nor derived from an argument to
frob (e.g. through a const_cast). We also assume that the programmer who makes the problematic call to
frob (the client programmer) isn't the one who implemented
frob (the library programmer).
Now I can think of a few ways in which the problem could emerge, but I think none of these is very realistic. I'm open to your suggestions and opinions.
Note: I'm not denying that the problem is technically possible, I just don't think it will occur under realistic assumptions.
1.
imp is a mutable global variable which is included through the header of the library.
Apart from <iostream>, where the use of mutable global variables is more or less necessary, nobody would trust a library that works with such global variables . Even in exceptional libraries such as <iostream> where there is an understandable reason to do it, no sane library writer would provide functions that
both take an argument of type
T and modify the global variable as a side effect. Not just because it's error-prone but also because there is no sensible use for it (try to imagine such a thing to see what I mean).
2.
frob is a non-const member function of an object of which
imp is a data member, and that same object allows clients to retrieve a (const) reference to
imp.
This would be something like a std::complex<
T> where real() returns a reference instead of a copy and where
frob is a calculation over a
T on the one hand and a std::complex<
T> on the other hand, modifying the latter in place. Note that <complex> is not like that because it's bad interface design and that nobody would want to use it otherwise. Still, in this case the client programmer would be consciously passing a data member of the parent object to a non-const member function of that same object, so to expect that the argument passed to
frob remains unmodified would be a direct contradiction.
3.
frob is a
const member function of an object of which
imp is a data member, and that same object allows clients to retrieve a (const) reference to
imp.
This would basically mean that the library programmer is actively misleading the client programmer, i.e. sabotaging the program on purpose.
4.
frob is a member function of
imp itself.
This is the only realistic scenario so far: the assignment family of operators are expected to work this way (so
frob could be one of those operators in this case). But if you assign an object to itself, you are justified to expect that it remains unchanged, and if you modify-assign it to itself inplace, you will always expect it to change. Also note that you can't take an argument by const reference and then (modify-)assign anything to it, including itself. So here the problem does not actually apply.
Why am I making this point? Because I think it illustrates how passing by const reference for all practical purposes still guarantees that the source object will not be changed by the receiving function. For multi-threaded applications const references are less helpful than "immutable references" (because in the former case the receiving function cannot rely on the value not to change), but for single-threaded applications I think they're practically equivalent.