Boost logo

Boost Users :

From: David Abrahams (dave_at_[hidden])
Date: 2004-12-22 14:34:57


Ronald Garcia wrote:
>>>>>>> Around here (the Open Systems Lab at Indiana University), I've
>>>>>>> been
>>>>>>> arguing against this property of C++ for a long time, specifically
>>>>>>> that
>>>>>>> the language does not allow you to bind temporary to a non-const
>>>>>>> reference. I am under the impression that this behavior is meant
>>>>>>> to
>>>>>>> "protect" the programmer from making certain mistakes, but the
>>>>>>> grave
>>>>>>> downside to this is that it also prevents a library writer from
>>>>>>> using
>>>>>>> proxy objects to mimic reference semantics.
>>>>>>
>>>>>> The only case I can think of where that's true is when the
>>>>>> reference
>>>>>> type is the same as the value type. If we allowed operator->
>>>>>>
>>>>> Is this a complete thought, or am I missing something?
>>
>> I think I was going to say that if we allowed it to return a non-const
>> pointer when is_same<reference, value_type>::value == true, that would
>> handle the cases in question.
>>
>
> Okay. For multi_array specifically, that doesn't work, because the
> reference type is a subarray (faux references) but the value type is a
> multi_array.
>
>
>>>> But do they need to? Why not have
>>>>
>>>> subarray const&
>>>>
>>>> behave like
>>>>
>>>> subarray&
>>>>
>>>> and
>>>>
>>>> const_subarray const&
>>>>
>>>> behave like
>>>>
>>>> const_subarray&
>>>>
>>>
>>> I had thought about this at the time and came to the following
>>> conclusion (modulo the fog of time since I made the decision):
>>>
>>> From my perspective, it seems that if a template function has a
>>> signature such as:
>>> template <Container>
>>> void my_function(Container& A);
>>>
>>> Then that function expects to receive a mutable container type. In
>>> the
>>> case that a function expects to receive an immutable container, the
>>> signature is likely to be:
>>>
>>> template <Container>
>>> void my_function(Container const& A);
>>>
>>> IIRC some templated functions are overloaded as above specifically to
>>> handle these two cases.
>>
>> Yes. Those functions are, in my experience, always used to expose
>> references/pointers that have the right mutability. For example,
>>
>> template <Container>
>> typename Container::value_type& operator[](Container&);
>>
>> template <Container>
>> typename Container::value_type const& operator[](Container const&);
>>
> I'm a little confused by this example. When would you pass a container
> to operator[]?

Those are meant to be free functions and I left out the 2nd argument.
Pardon me. Try:

   Container::value_type& operator[](int);

   Container::value_type const& operator[](int) const;

> Or is that irrelevant to the example you are
> describing? I am trying to consider functions /that I did not write/
> are generic on a container type. The above looks like an
> implementation detail of a container library to me.

Okay, let's try again:

   template <class Container>
   typename Container::value_type& at(Container& c, int i)
   {
        if (i < 0 || i > c.size()) throw out_of_range();
        return c[i];
   }

   template <class Container>
   typename Container::value_type const& at(Container const& c, int i);
   {
        if (i < 0 || i > c.size()) throw out_of_range();
        return c[i];
   }

My point stands: this kind of overload is always used to expose internal
pointers and references with the right constness.

> Furthermore,
> wouldn't an operator[]() return reference type? If reference and
> value_type are not the same type, does what you describe here still
> apply?

Sorry that my example was so confusing.

But no, you can't use reference here instead of value_type if you are
concerned with const-correctness because for any ordinary container C, C
and C const have the same ::reference type. Although all of that is
irrelevant to the point. My point is that you are saying "overloading
on const is important and always returning const proxies breaks that
functionality." I am saying there's only one reason people overload on
const, and there are other ways to handle that.

Generic libraries *would* need to do something special to manage these
cases. The 2nd at() signature above won't work. You need something
more like:

 template <class Container>
 typename mutable_reference<Container>::type at(Container& c, int i)

 template <class Container>
 typename constant_reference<Container>::type at(Container const& c, int i);

where

  constant_reference<mutable_subarray>::type

is

  mutable_subarray::value_type&

and

  mutable_reference<constant_subarray>::type

is

  constant_subarray<value_type> const&.

>> They _never_ have different operational semantics. There are several
>> ways to handle that with const proxies. For example,
>>
>> const_subarray<T>::value_type
>>
>> could be the same as
>>
>> subarray<T>::value_type const
>>
>>> I concluded that if I chose the design you describe above, namely that
>>> "subarray const&" behave like "subarray&" and "const_subarray const&"
>>> behave like "const_subarray&", then I would likely violate the
>>> expectations of a template function. Objects of type subarray , the
>>> mutable reference proxy, would be passed to the second function above.
>>
>> I don't think that's a big deal.
>>
> I am presuming that you feel that way because "They _never_ have
> different operational semantics" right?

Yes.

>>> Suppose for a moment that a one-dimensional subarray is passed to the
>>> above function. Using the above design, A::operator[] would return a
>>> mutable reference to an underlying data element, though a const
>>> reference to the underlying data might have been expected. That
>>> mutable reference could be accidentally mutated (for example, by a
>>> similarly overloaded function that mutates in one case and does not in
>>> the other).
>>
>> I don't think so; see above.
>>
> Could you elaborate here?

I'm saying that A::operator[] would never return a mutable reference
unless A was non-const or was a (const or non-const) mutable_subarray.
As a baseline, we'd never violate const correctness.

> Are you again referring to "They _never_
> have different operational semantics"

No, but that's a good point.

>> Understandable, but perhaps this an example of the insufficient
>> imagination I was alluding to.
>>
> Actually I am more worried that my imagination is excessive in certain
> respects. My concern at the moment is quote I keep referring to
> above. To be honest I have no example of two such functions having
> different "operational semantics", by which I assume you mean that the
> actual text of the two functions is identical, but I can imagine
> hypothetically that such a case could exist. That's where my
> imagination may be going too far.

Such a case would be deadly for usability. Imagine:

  template <class C>
  void some_algorithm(C& c)
  {
      ...
      some_other_algorithm(c);
      ...
  }

If some_other_algorithm changed its behavior depending on whether C was
const, users of some_algorithm would be very surprised when they passed
a const vector.

>> The way I see it, you're asking me to sacrifice the ability to provide
>> const-correctness for convenience, and I'm suggesting that no matter
>> how ugly it may be, there's a way to keep everything safe within the
>> current language.
>>
>
> I seems to me that "keeping everything safe within the current
> language" relies on the assumption that you make above, that I quote
> multiple times, and that's a statement not about the language, but
> about the behavior of programmers, which of course is fine so long as
> it's true and that no good reason exists for straying from that
> prescription.

Let's just say that if programmers violate my assumption, returning
const rvalue proxies is just going to add another drop to the bucket of
confusion that they have already created.

> The way I see it, the current ban on binding temporaries
> to const references is a language level prescription of the same sort
> you describe above: that it protects people from making so-called
> "mistakes" at the expense of capabilities that are useful to library
> developers. I hope that the Move Semantics proposal makes this
> conversation irrelevant in the end

I have some doubts that it will. After all, people aren't going to stop
writing functions that take T& arguments. Even making a proxy movable
won't cause it to bind to a T&, IIUC. I think we need a way to express
that non-const rvalues of a particular class type should bind to T&
(with T deduced as non-const). Perhaps just adding a conversion
operator to T& could serve, given the appropriate language extension.

> and that the iterator library can
> take advantage of what it adds to the language. Where can I find a
> most recent copy of that proposal, btw?

It's in the pre-redmond mailing, in the usual place. You can ask howard
if you want more-recent unpublished work.

> Since iterator adaptors works
> just fine today, I'm far more interested in how other aspects of the
> language will change with respect to these issues. Thanks for the
> responses. I think that they have somewhat clarified my understanding
> of the issues.

-- 
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com

Boost-users list run by williamkempf at hotmail.com, kalb at libertysoft.com, bjorn.karlsson at readsoft.com, gregod at cs.rpi.edu, wekempf at cox.net