Proxy objects and casting to rvalues

121 views
Skip to first unread message

Aarón Bueno Villares

unread,
Apr 9, 2017, 2:21:02 AM4/9/17
to ISO C++ Standard - Future Proposals
Hello! And sorry for my english.

Let's consider that example, taking from one comments of the Eric Niebler's blog:

    std::vector<bool> v{true,false,true};
    
    for (auto i : v)
        i = false;  // (1)

The range-based for is modifying the vector, since it's taking copies of the proxy object. However, the proxy object is a reference-wrapper. And what is the property of a reference? That if I need to get a copy, a need a copy of the value type, not the reference type. I have reading some attempts to propose auto deduction rules and the sort, but the seems not satisfaying But, why not adding to the language the posibility of specifying a dual type according if they is being used as reference or value type?

For example:

    struct Iterator
    {
        // other code
        value_type T;
        reference R;

        reference ! value_type operator*() const { return reference(/**/); }
    };

    Iterator it = container.begin();

    auto& ref = *it; // Compilation error, obvious. `reference` is a rvalue.
    auto const& ref = *it; // Ok, deduced as reference const&

    *it = something; // Ok, the proxy still takes control.

    auto value = *it; // Deduced as value_type! I have a copy of the real value!!
    reference value = *it; // Compilation error?

The reference type, of course, must be convertible to the "after !" type. I have given the example with the iterator case, but it can be applied to any non-void function. The choice of `!` is because no specific reason. I just thought that it can be easier for parsers, since can't be confused with any other declarator.

For the `vector<bool>` case:

    template<...>
    class vector<bool, ...>
    {
           using value_type = bool;
           using reference = bit_wrapper;

           reference ! value_type operator*() const { return bit_wrapper(/**/); }
    };

Or even better:

    template<...>
    class vector<bool, ...>
    {
           using value_type = bool;
           using reference = bit_wrapper ! value_type;

           reference operator*() const { return bit_wrapper(/**/) }
    };

That way, the semantics of the `reference` and `value_type` are keep separated. It just specifies what happens when copying. The `auto` deduction rules aren't modified, since, when copying, a cast is performing. What is seen by auto is the type of the temporary object result of the casting. What changes is the effects of the copy operation, that before doing a copy, a casting is performed (I don't know what to say about moving the combined type).

    auto const& ref = *it;
    auto val = ref;

`ref` is deduced as `bit_wrapper ! bool const&" (a.k.a bit_wrapper const& untill is copied), and val is deduced as "bool". The idea is to specify that a type must fallback to another type when using it as value, and the type must be of course convertible to it. That shouldn't be specified in the `type` definition (inside the class). The `type` is unconnected to what types can be combined to (except that it must be convertible to, but a conversion can be available without having that posible combination in mind in the first place). You could even create variables or returning objects of those combined types.

    wrapper ! value_type val(...); // I'm creating a true reference wrapper (wrapper, by itself, is not a true reference till combined with its value type).
    auto v = val; // v is value_type.

But it's harder to say what will be the different rules about the combined types in the different expressions it could appear, and I know that is a very big change for a very specific case (proxy "things"), but I think, if the rules are well design and very enclosed to only what it's needed, maybe it can be, maybe and at least, arguable, without touching current STL's algorithm implementations.

Aarón Bueno Villares

unread,
Apr 9, 2017, 2:44:19 AM4/9/17
to std-pr...@isocpp.org
My first paragraph is closed of not being understandable, sorry:

The range-based-for is modifying the vector, since it's taking copies of the proxy objects. However, each proxy object is a reference-wrapper. And what is the property of a reference? That if I need to get a copy, I need a copy of the value type, not the reference itselft. I was reading some attempts to propose "auto operators" and the sort, but they seemed not satisfying. But, why not adding to the language the posibility of specifying dual types according to if the type is being used as reference or value type?

 

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/9da2eb61-ab97-4b03-933e-da18139d77c0%40isocpp.org.

Nicol Bolas

unread,
Apr 9, 2017, 10:30:28 AM4/9/17
to ISO C++ Standard - Future Proposals
What you're essentially asking is a modified form of something that has been discussed since `auto` came into existence: the ability to specify the type that will be deduced by `auto`. And proxy objects are the primary justification for it, whether it is proxy iterators or lazy evaluation of expressions (you want `auto` to evaluate the lazy expression, not store the lazy intermediate).

The best syntactic form I've seen this in is via a member function of the form:

class bit_wrapper
{
public:
   
Typename operator auto() const { <conversion expression> }
};

Note that this allows you to do arbitrary work in the conversion section. Though we might add an `= default` version which is equivalent to implicit conversion via `return *this;`.

This function is put in the actual type that would otherwise be deduced. I think this works much better than your notion of adding it to the `operator*` of the iterator (I assume; you put it in `vector<bool>` instead, which has no `operator*`). Your way requires the compiler to change what it is doing based on the return of a specific function call, so it requires more work to do lazy expression evaluation, since you have to apply it to every single function that returns a lazy result. Whereas here, you just apply this to the lazy result types, and every use of those types will automatically have this power.

The big, unanswered question with such a feature is mainly about what exactly it affects. Does it only affect uses of the `auto` keyword for variables? If it only affects `auto` variables, would that not be strange with regard to generic lambda/Concepts TS `auto` parameters, which use template argument deduction? Does it affect `decltype(auto)`, and if so, how exactly? If it affects every form of deduction (template argument, `decltype(auto)` and `auto`), then exactly how does it work?

Aarón Bueno Villares

unread,
Apr 9, 2017, 11:51:46 AM4/9/17
to ISO C++ Standard - Future Proposals

On Sunday, 9 April 2017 16:30:28 UTC+2, Nicol Bolas wrote:
What you're essentially asking is a modified form of something that has been discussed since `auto` came into existence: the ability to specify the type that will be deduced by `auto`. And proxy objects are the primary justification for it, whether it is proxy iterators or lazy evaluation of expressions (you want `auto` to evaluate the lazy expression, not store the lazy intermediate).

The best syntactic form I've seen this in is via a member function of the form:

class bit_wrapper
{
public:
   
Typename operator auto() const { <conversion expression> }
};

Note that this allows you to do arbitrary work in the conversion section. Though we might add an `= default` version which is equivalent to implicit conversion via `return *this;`.


But I think it makes impossible to use `auto` to copy instances of that type. For instance:

```C++
struct wrapper { value_type operator auto() const { /**/ } };
auto wrapper_obj = wrapper_factory::give_me_a_new_wrapper(); 
```

or any copy I have to do with the wrapper (or any intermediary function returning it), will be deduced as `value_type`. However, I'm still working with a `wrapper`!! The "reference nature" has not been start yet. That conversion operator forces me to think a bit more anytime I want to use `auto`. 

What if I want to bind the object to a reference, to modify the real object that is behind?

```C++
auto const& w = *it;
w = something; // The wrapped object must be modified, as a real reference.
```

Since the proxy represents a reference, must be trated as such when taking it by reference or by value when it is still a "object proxy". But when passing it as "pure proxy" or "pure reference proxy" or the way you want to call it, then it must be cast to the secondary type when copying. The `! value_type` is not a new type, but some kind of conversion qualifier.
 
This function is put in the actual type that would otherwise be deduced. I think this works much better than your notion of adding it to the `operator*` of the iterator (I assume; you put it in `vector<bool>` instead, which has no `operator*`). Your way requires the compiler to change what it is doing based on the return of a specific function call, so it requires more work to do lazy expression evaluation, since you have to apply it to every single function that returns a lazy result.


Yes, the whole thing it's crazy complex semantically. Too much context.

The big, unanswered question with such a feature is mainly about what exactly it affects. Does it only affect uses of the `auto` keyword for variables? If it only affects `auto` variables, would that not be strange with regard to generic lambda/Concepts TS `auto` parameters, which use template argument deduction? Does it affect `decltype(auto)`, and if so, how exactly? If it affects every form of deduction (template argument, `decltype(auto)` and `auto`), then exactly how does it work?

My approach has the same problems. The thing is that, when you are constructing a new object from a combined type, you have too much situations (whe asume that `value_type` cannot be a reference, but `wrapper` does):

```C++
/* When expr is T ! V (T is not referfence) */
// (1)
T a = expr; // Error or do we want to copy the proxy object losing the convert-when-copying nature?
T& ra = expr; // Same situation.
T const& ra = expr; // Same situation.

// (2)
T!V a = expr; // Ok, we copy `T`.
T!V& a = expr; // Ok, if expr returns an `lvalue`.
T!V const& a = expr; // Ok.

// (3)
auto a = expr; // Ok, we cast to V and then copy. auto decuded as V
auto& a2 = expr; // auto deduced as T!V& (a.k.a T&)
auto const& a3 = expr; // auto decuded as T!V const& (a.k.a T const&).
auto a4 = a2; // Ok, auto deduced as V since a2 is T!V&
```

The think is that, when dealing with iterators, you don't have two types, but have three:

* value_type represents the storage. What `sort` needs when doing copies.
* sequence_type represents the view that the iterator gives to you.
* reference is something in the middle, with allows to comunicate between both worlds. What `operator*` returns.

`sequence_type` doesn't need to be explicitely defined, not added to the Iterator concept or something. It is what users expect when reading the iterator (for example, the parameters of the functors passed to `for_each`). With `reference` being convertible to user parameters is enough. `value_type` is what algorithms and user expects for local storage, to transform and save back the value to the iterator. The `reference` type is then tricked to allow one of another usage depending on the context, in a way more or less, transparent to current implementations. 

Nicol Bolas

unread,
Apr 9, 2017, 2:00:35 PM4/9/17
to ISO C++ Standard - Future Proposals
On Sunday, April 9, 2017 at 11:51:46 AM UTC-4, Aarón Bueno Villares wrote:

On Sunday, 9 April 2017 16:30:28 UTC+2, Nicol Bolas wrote:
What you're essentially asking is a modified form of something that has been discussed since `auto` came into existence: the ability to specify the type that will be deduced by `auto`. And proxy objects are the primary justification for it, whether it is proxy iterators or lazy evaluation of expressions (you want `auto` to evaluate the lazy expression, not store the lazy intermediate).

The best syntactic form I've seen this in is via a member function of the form:

class bit_wrapper
{
public:
   
Typename operator auto() const { <conversion expression> }
};

Note that this allows you to do arbitrary work in the conversion section. Though we might add an `= default` version which is equivalent to implicit conversion via `return *this;`.


But I think it makes impossible to use `auto` to copy instances of that type. For instance:

```C++
struct wrapper { value_type operator auto() const { /**/ } };
auto wrapper_obj = wrapper_factory::give_me_a_new_wrapper(); 
```

Yes. That is in fact the whole point.

Pre-`auto`, you would have had to explicitly state the name of the proxy object's type if you wanted to store it. That prevented people from accidentally storing proxy objects, since proxy types are usually internal objects who's names and data aren't really part of the interface. People would instead use the base object's type and let implicit conversion handle things.

With `auto`, you now get the proxy object's type. But that's not what you want most of the time.

By giving a type an `operator auto` function, we revert back to the pre-C++11 behavior: if you want to store the proxy object, you must name it.

or any copy I have to do with the wrapper (or any intermediary function returning it), will be deduced as `value_type`. However, I'm still working with a `wrapper`!! The "reference nature" has not been start yet. That conversion operator forces me to think a bit more anytime I want to use `auto`.

Would it?

Consider a string class that uses lazy expression evaluation. You do this:

auto str = someString + otherString + moreString + StringType("literal");

What does the user expect `str` to be? Odds are very good that the user wanted `str` to be a string value, not a lazy expression object. Especially not a lazy expression object that references a temporary that has now been destroyed.

Pre-C++11, the user would have typed:

StringType str = someString + otherString + moreString + StringType("literal");

But post-C++11, the user would rather use `auto`. Except that they cannot, because this will give them something other than `StringType`.

Note that by having `auto` deduce the proxy type, we introduce a bug: the temporary stored by the lazy expression object is destroyed. By using `operator auto`, we prevent that bug from being accidentally encountered. It can only be encountered by explicitly naming the proxy object type. And that makes it clear to everyone that there's a bug present.
 
What if I want to bind the object to a reference, to modify the real object that is behind?

Then you have to name the type.

You have to remember: you rarely want to play with proxy objects directly. They're mediators between some internal representation and the actual value you want to talk to. They're an implementation detail.

The common case use for proxies is to convert them to their real type; that's what people do with them 90% of the time. As such, if you don't want that, then you're going to have to name the type explicitly.

Reply all
Reply to author
Forward
0 new messages