
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const. 0) Const-ness depends on the const-ness of the object being proxied: struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::iterator const_iterator; iterator begin() const { return vec.begin(); } ... }; 1) Const-ness depends on the proxy object itself: struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::const_iterator const_iterator; iterator begin() { return vec.begin(); } const_iterator begin() const { return vec.begin(); } ... }; I think a loooong time ago, I preferred (1) but these days (0) is more natural for me, but I can't say why, exactly. Just feels like that's how it should be. Thoughts? FWIW, it came up in the context of BOOST_FOREACH's handling of proxies, so it's not (far) off-topic. -- Eric Niebler Boost Consulting www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

Eric Niebler wrote:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::iterator const_iterator; iterator begin() const { return vec.begin(); } ... };
1) Const-ness depends on the proxy object itself:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::const_iterator const_iterator; iterator begin() { return vec.begin(); } const_iterator begin() const { return vec.begin(); } ... };
I think a loooong time ago, I preferred (1) but these days (0) is more natural for me, but I can't say why, exactly. Just feels like that's how it should be. Thoughts?
If the vector_proxy has an ordinary copy constructor, (0) is usually correct because (1) cannot be enforced. vector_proxy const a = ...; vector_proxy b = a; (1) typically only makes sense for types that are intended to be class members and propagate the constness of the member function to the proxied (referred to) object for some reason. (1) can work for move-only types. I don't have experience with such proxies, so I can't tell. :-)

Peter Dimov wrote:
Eric Niebler wrote:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
1) Const-ness depends on the proxy object itself:
<snip>
If the vector_proxy has an ordinary copy constructor, (0) is usually correct because (1) cannot be enforced.
vector_proxy const a = ...; vector_proxy b = a;
That's a good point, and very relevant to the BOOST_FOREACH discussion because BOOST_FOREACH must make a copy of the proxy.
(1) typically only makes sense for types that are intended to be class members and propagate the constness of the member function to the proxied (referred to) object for some reason.
Right, that would seem to be a legitimate use.
(1) can work for move-only types. I don't have experience with such proxies, so I can't tell. :-)
You mean when the proxy is move only? How does that solve the problem above? vector_proxy const a = ...; vector_proxy b = std::move(a); I've probably misunderstood. -- Eric Niebler Boost Consulting www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

Eric Niebler wrote:
Peter Dimov wrote:
(1) can work for move-only types. I don't have experience with such proxies, so I can't tell. :-)
You mean when the proxy is move only? How does that solve the problem above?
vector_proxy const a = ...; vector_proxy b = std::move(a);
This won't work; the move constructor only takes non-const rvalues.

On 7/24/07, Martin Schulz <Martin.Schulz@synopsys.com> wrote:
I tend to disagree. I think of an proxy object not so much as a "reference" but rather as a "drop-in replacement" that happens by chance to be connected to some other container object, but that is only an implementation detail.
If the proxy models an STL container then it should work with any STL or
other appropriate algorithm and I do not consider this a hack. It simply works as designed.
If it were possible, it would be useful to be able to think about proxies like that, but it misses out on the fact that a proxy to a container in C++ generally is not a model of a standard container, at least not practically, so you generally can't think of a proxy as simply a drop-in replacement, and in practice, because of this, you also can't safely pass a proxy to an algorithm where a container truely is required. You'd do so only with algorithms where a smaller subset of the container requirements are required. As an example of requirements you likely would not want a proxy to meet, consider the constructor and destructor requirements of standard containers (sequences pose even more problems). In order to be a container, copying a proxy would have to imply creating a new container, though the implementation shown above would just produce another proxy which references the same container. This already makes the "drop-in replacement" view not true and is an example of why a proxy as a container itself generally isn't even the desired behavior. Destruction poses another problem, since destruction of a container is required to destroy all of the contained elements. In practice, implementing either requirement is generally not desireable for a proxy. The purpose of a proxy is generally to introduce a partially transparent level of indirection such that you may simulate reference semantics when code is written in places where reference or value semantics may be safely used. So yes, I would say passing a proxy where a container is listed as a requirement would definitely be a hack, unless you can make it truely meet all of the requirements listed above. Usually you can get away with ignoring this fact and your code will manage to compile and work, but consider for a moment a generic algorithm in C++0x which explicitly requires a reference to a standard sequence as an argument. Now the requirement is enforced rather than simply being documented. Here, passing a vector works perfectly fine, as it should, since a vector is a model of a sequence. However, now try to pass a proxy. In order to even have your code compile, the proxy itself would have to be a valid standard sequence, explicitly meeting all of the associated requirements, including the troublesome requirements for proxies mentioned above. Note that this type of issue exists in current C++ code that specifies such requirements in documentation, only the language isn't going to catch your logical error for you, and instead you will just be violating your own contracts. If you make the requirements for the algorithm less strict such that both a proxy and a container itself meet the requirements, then all is fine and correct, given that you interface with the stored data in a way which works with objects and with proxies, but if you instead lie in code and state that the proxy is a valid sequence when it is not, knowing that internally the algorithm doesn't actually need to rely on all of the specified requirements, it is simply a hack to get around a design flaw. That's not to say that proxies aren't useful, it just means that in general, they are not true drop-in replacements that go anywhere the target type would go, unlike what many people tend to believe, and if your proxy doesn't correctly model the concepts that your referenced object models, then you have made a logical error if you attempt to use it where those requirements are specified, whether your code seems to work or not. Because of this, you should try to provide as much functionality as you can when implementing the proxy, but you can't ignore the fact that a level of indirection still exists. Therefore I favor option 1).
Clearly a non-const proxy may not be constructed from a const container.
When I pass some container-alike object into some method e.g. as const
reference, I do not expect its contents to be modifyable.
As was pointed out by Peter and Eric, when copying proxies for example, this type of logic can't even be enforced, and I argue that this is because at a higher level, your proxy is still logically dealing with a level of indirection, yet you are incorrectly attempting to combine the cv-qualification of the target with the cv-qualification of the proxy, despite the fact that they are two logically different concepts, only somewhat blurred by the fact that much of the interfacing is automatically forwarded. You can try to force certain meaning out of the top-level qualification that "just works" for many, though not all, algorithms, depending on how they are written, but then things start to get hairy since you are essentially altering the meaning of top-level cv-qualification such that it propogates through and is inseparable from the qualification of the target type. Unless your proxy manages to implement true value semantics, I don't see how you can logically allow the qualification to fall through.
members and propagate the constness of the member function to the
(1) typically only makes sense for types that are intended to be class proxied
(referred to) object for some reason.
Right, that would seem to be a legitimate use.
I agree that if you wanted such functionality this would work fine, however, I would personally like to see somewhere in practice that this actually is the desired behavior and where what you are dealing with truely is a proxy -- explicitly noting the requirements of the algorithm or datastructure. I don't doubt that it is possible that such cases may potentially exist, though I am not convinced, and offhand, none jump immediately to mind. -- -Matt Calabrese

As an example of requirements you likely would not want a proxy to meet, consider the constructor and destructor requirements of standard containers (sequences pose even more problems).
Matt, thank you for your thoughts. My impression is that when I hear the word "proxy", I rather think of a certain object, while you rather seem to think about some specific classes. Yours, Martin.

On 7/23/07, Eric Niebler <eric@boost-consulting.com> wrote:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
1) Const-ness depends on the proxy object itself:
I suppose it really depends on context and how exactly you wish to deal with the proxy, but I would personally try to keep semantics as close as possible to a reference type. You can't directly create a reference type with cv-qualification, and by convention, metafunctions which add cv-qualification to reference types generally yield the input reference type unmodified (such as in Boost.Type_Traits). You obviously can't mimic the former behavior, but to come as close as possible to reference behavior, I'd make cv-qualification of the proxy type not impact the view of the proxied object, and instead make it as much of an implementation detail as possible. Jumping away for a second, given a reference to a vector as a datamember to some type, when this encapsulating type becomes const-qualified, your reference datamember does not all of a sudden reference the vector as though it were const-qualified. Similarly, if you have a proxy to a vector as a datamember, when the encapsulating type becomes const-qualified, your proxy should not all of a sudden reference the vector as though it were const-qualified. You are still dealing with reference semantics so the top-level qualification should not propogate. Because of this, I'd say 0 is the better approach, or at least the most consistent approach with respect to the rest of the language. Honestly, though, I'm not in love with either approach. As I said, I would prefer 0 over 1, but 0's definition of vector_proxy::const_iterator is still troubling to me. Perhaps for practical reasons, such as use with certain generic algorithms, it may make sense to have vector_proxy::const_iterator be the same type as std::vector<int>::iterator as you have shown, but if you absolutely must have the typedef present, I would personally more intuitively prefer it to be the same type as std::vector<int>::const_iterator, though still only provide the single begin() const with a return type of iterator. It really depends on how the proxy is used and what behavior your algorithms expect from the proxies. While likely not practical for many cases, I would even consider a third option, and that is to not include the nested typedefs at all. After all, there is no such thing as std::vector<int>&::iterator. For the sake of hacking together proxies that work with algorithms which expect STL containers, providing the nested typedefs may be the only option to optimize code reuse, but in those cases, you have to realize that what you would be doing truely is a hack, since a proxy itself is not a valid STL container type and therefore shouldn't be passed to algorithms where one is expected. -- -Matt Calabrese

For the sake of hacking together proxies that work with algorithms which expect STL containers, providing the nested typedefs may be the only option to optimize code reuse, but in those cases, you have to realize that what you would be doing truely is a hack, since a proxy itself is not a valid STL container type and therefore shouldn't be passed to algorithms where one is expected.
I tend to disagree. I think of an proxy object not so much as a "reference" but rather as a "drop-in replacement" that happens by chance to be connected to some other container object, but that is only an implementation detail. If the proxy models an STL container then it should work with any STL or other appropriate algorithm and I do not consider this a hack. It simply works as designed. Therefore I favor option 1). Clearly a non-const proxy may not be constructed from a const container. When I pass some container-alike object into some method e.g. as const reference, I do not expect its contents to be modifyable. For some more thoughts about const and non-const iterators, see also http://www.boost.org/libs/iterator/doc/iterator_facade.html#a-constant-n ode-iterator Yours, -- Dr. Martin Schulz (schulz@synopsys.com) Software Engineer Synopsys GmbH Karl-Hammerschmidt-Str. 34 D-85609 Dornach, Germany Phone: +49 (89) 993-20203 http://www.synopsys.com

Matt Calabrese wrote:
On 7/23/07, Eric Niebler <eric@boost-consulting.com> wrote:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
1) Const-ness depends on the proxy object itself:
I suppose it really depends on context and how exactly you wish to deal with the proxy, but I would personally try to keep semantics as close as possible to a reference type.
<snip discussion> Yes, I thought of this, but there is another viewpoint; namely, to make proxy<T> behave as much like a T as possible. (I'm playing devil's advocate here -- I don't espouse this view but I'm not convinced it's wrong, either.) For instance, in generic code, you might overload a foo() on const and non-const arguments: template<typename T> void foo(T &t); // T is mutable template<typename T> void foo(T const &t); // T is not mutable In places like this, top-level const is used to determine mutability. So if you have a proxy in the style of (1), it might end up in the wrong overload. I don't generally write code like this, but my use of proxies in the style of (0) has occasionally forced me to write a const overload where it wouldn't otherwise be needed just to attract such proxies. So, why in your opinion is the "proxy as reference" viewpoint to be preferred over the "proxy as value" viewpoint? I know the answer in my gut, but so far I haven't really heard a killer argument. Peter's argument about copies of the proxy not being able to enforce their const-ness is pretty convincing, though. And this was Peter Dimov's counter example: struct Foo { proxy<std::vector<int> > vec; }; Maybe the author of Foo wants deep-const here: if a Foo object is const, then the vector should be const as well. Again, I don't write code like that, but I don't think there's anything wrong with it. -- Eric Niebler Boost Consulting www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

On 7/24/07, Eric Niebler <eric@boost-consulting.com> wrote:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::iterator const_iterator; iterator begin() const { return vec.begin(); } ... };
1) Const-ness depends on the proxy object itself:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::const_iterator const_iterator; iterator begin() { return vec.begin(); } const_iterator begin() const { return vec.begin(); } ... };
I think a loooong time ago, I preferred (1) but these days (0) is more natural for me, but I can't say why, exactly. Just feels like that's how it should be. Thoughts?
FWIW, it came up in the context of BOOST_FOREACH's handling of proxies, so it's not (far) off-topic.
Consider this: std::vector<int> x = .... for_each(filter(x, some_filter_op), do_something); 'Filter' will obviously return a proxy for 'x' (actually wrapping x iterators with boost::filter iterators) as a temporary object. for_each will bind the temporary to a const reference (at least until we get rvalue references). This means that with option 1, do_something will get const references to int, thus cannot mutate the integers in x. With option 0, do_something will get non const references and can actually mutate x, and this is, I think, what would normally be expected. I write lots of code like this and deep constness semantics would really make it impractical. (side effect free code is nice, but sometimes you just need them). Even if you consider simple proxies that do not add any semantics. I would expect this code to do the right thing: std::vector<int> x; for_each(make_range(x.begin(), x.end()), _1 = 0) (btw, notice that if you substitute make_range with boost::iterator_range this works as expected, while boost::sub_range has deep const semantics.) just my 0.02 euros gpd

Giovanni, sorry for the late reply.
Consider this:
std::vector<int> x = .... for_each(filter(x, some_filter_op), do_something);
'Filter' will obviously return a proxy for 'x' (actually wrapping x iterators with boost::filter iterators) as a temporary object. for_each will bind the temporary to a const reference (at least until we get rvalue references).
The fact that temporary objects cannot be passed in as non-const references had been decided to avoid programming accidents in that modifications to the temporary object were assumed to be meaningfull and should be preserved. If you want this object as non-const nevertheless, simply give a name to it.
This means that with option 1, do_something will get const references to int, thus cannot mutate the integers in x.
But with a slight modification I do not see any more problem: std::vector<int> x = .... filter_type f = filter(x, some_filter_op); for_each(f, do_something); Here, f is passed as non-const reference and can thus be modified. Tough some more effort is needed to provide the correct return type of the filter function; I do not see yet any fundamental obstacle to this approach. Yours, Martin.

on Mon Aug 13 2007, "Martin Schulz" <Martin.Schulz-AT-synopsys.com> wrote:
Giovanni, sorry for the late reply.
Consider this:
std::vector<int> x = .... for_each(filter(x, some_filter_op), do_something);
'Filter' will obviously return a proxy for 'x' (actually wrapping x iterators with boost::filter iterators) as a temporary object. for_each will bind the temporary to a const reference (at least until we get rvalue references).
The fact that temporary objects cannot be passed in as non-const references had been decided to avoid programming accidents in that modifications to the temporary object were assumed to be meaningfull and should be preserved. If you want this object as non-const nevertheless, simply give a name to it.
That's less simple than you make it sound. It could have a very complicated type. One good way to deal with this is to make the proxy object return a constant object. Then it will bind to a T& parameter. Furthermore, the constness of the proxy should not affect the constness of its access to elements of x. That essentially means returning a different proxy type depending on whether the incoming container is const or not. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

On 8/13/07, David Abrahams <dave@boost-consulting.com> wrote:
on Mon Aug 13 2007, "Martin Schulz" <Martin.Schulz-AT-synopsys.com> wrote:
Giovanni, sorry for the late reply.
Consider this:
std::vector<int> x = .... for_each(filter(x, some_filter_op), do_something);
'Filter' will obviously return a proxy for 'x' (actually wrapping x iterators with boost::filter iterators) as a temporary object. for_each will bind the temporary to a const reference (at least until we get rvalue references).
The fact that temporary objects cannot be passed in as non-const references had been decided to avoid programming accidents in that modifications to the temporary object were assumed to be meaningfull and should be preserved. If you want this object as non-const nevertheless, simply give a name to it.
That's less simple than you make it sound. It could have a very complicated type.
Yep, exactly. And when you throw boost.lambda in, your objects have types you do not even want to look at.
One good way to deal with this is to make the proxy object return a constant object. Then it will bind to a T& parameter.
That was what I did at the beginning, but I had to deal with lots of third party functions that didn't constify their arguments, so I ended up writing a forwarding layer that provides all const/non-const overloads up to 5 arguments (I could have used the one provided by fusion, but I'm stuck with boost 1.32, so I have to reinvent the wheel over and over). All my range functions are actually function objects (for bind compatibility) so this works very well.
Furthermore, the constness of the proxy should not affect the constness of its access to elements of x. That essentially means returning a different proxy type depending on whether the incoming container is const or not.
Agree 100%. gpd

on Mon Aug 13 2007, "Giovanni Piero Deretta" <gpderetta-AT-gmail.com> wrote:
One good way to deal with this is to make the proxy object return a constant object. Then it will bind to a T& parameter.
That was what I did at the beginning, but I had to deal with lots of third party functions that didn't constify their arguments
Example? I don't see why that should make a difference. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

On 8/13/07, David Abrahams <dave@boost-consulting.com> wrote:
on Mon Aug 13 2007, "Giovanni Piero Deretta" <gpderetta-AT-gmail.com> wrote:
One good way to deal with this is to make the proxy object return a constant object. Then it will bind to a T& parameter.
That was what I did at the beginning, but I had to deal with lots of third party functions that didn't constify their arguments
Example? I don't see why that should make a difference.
Sorry, I meant result type, not argument: If a third party function (wich prototype I can't modify) returns a non const temporary vector, I have to design my range algorithms to work with non const temporaries anyway if I want to interoperate with it. gpd

on Mon Aug 13 2007, "Giovanni Piero Deretta" <gpderetta-AT-gmail.com> wrote:
On 8/13/07, David Abrahams <dave@boost-consulting.com> wrote:
on Mon Aug 13 2007, "Giovanni Piero Deretta" <gpderetta-AT-gmail.com> wrote:
One good way to deal with this is to make the proxy object return a constant object. Then it will bind to a T& parameter.
That was what I did at the beginning, but I had to deal with lots of third party functions that didn't constify their arguments
Example? I don't see why that should make a difference.
Sorry, I meant result type, not argument: If a third party function (wich prototype I can't modify) returns a non const temporary vector, I have to design my range algorithms to work with non const temporaries anyway if I want to interoperate with it.
yep. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com The Astoria Seminar ==> http://www.astoriaseminar.com

Eric Niebler skrev:
General design question here. This came up on the user's list, and it made me question something I believed to be true. When writing a proxy for, e.g., a std container, I see two options for handling const.
0) Const-ness depends on the const-ness of the object being proxied:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::iterator const_iterator; iterator begin() const { return vec.begin(); } ... };
1) Const-ness depends on the proxy object itself:
struct vector_proxy { std::vector<int> & vec; typedef std::vector<int>::iterator iterator; typedef std::vector<int>::const_iterator const_iterator; iterator begin() { return vec.begin(); } const_iterator begin() const { return vec.begin(); } ... };
I think a loooong time ago, I preferred (1) but these days (0) is more natural for me, but I can't say why, exactly. Just feels like that's how it should be. Thoughts?
There's a relevant piece in Boost.Range: http://www.boost.org/libs/range/doc/utility_class.html boost::iterator_range falls into case 1, whereas boost::sub_range falls into case 2. I think case one has been the preferred way in C++0x3 because the language lacks rvalue references. Without rvalue references we often say template< class Range > void SomeAlgo( const iterator_range<Range>& ); Wiht rvalue references, I think option 2 will gain popularity. -Thorsten
participants (7)
-
David Abrahams
-
Eric Niebler
-
Giovanni Piero Deretta
-
Martin Schulz
-
Matt Calabrese
-
Peter Dimov
-
Thorsten Ottosen