Discussion:
[std-proposals] Proposal: std::view and std::optional_view (Re: operator dot proposal)
j***@gmail.com
2016-10-11 03:10:37 UTC
Permalink
Hi,

This message highlights my thoughts about so-called "smart references" and
how best to support such a concept in the standard library.

I am usually excited by any upcoming or proposed additions to C++, but the operator
dot proposal
<http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4173.pdf>
concerns me. It strikes me as adding significant complexity to the language
to support what amounts to a bit of syntactic sugar. Of course, given that
the proposal essentially adds support for compile-time function overriding,
I'm sure it could be used to do all sorts of "clever" things, but I digress.

I do see the need for official support for "smart references" in C++.
Modern C++ orthodoxy says that raw pointers should be avoided in high-level
code. C++ has always recommended std::string over C-style strings and
std::vector over C-style dynamic arrays. The smart pointers std::unique_ptr
and std::shared_ptr are now established for resource management, and
std::array for statically sized arrays. The upcoming std::optional replaces
some potential uses of raw pointers, std::string_view means we no longer
need to deal with raw pointers into std::string objects, and the proposed
std::array_view will do the same for arrays.

However, one use of raw pointers still remains: as non-owning references.
When I say references, I don't mean just C++ references; I mean any use of
C++ pointers or C++ references to refer to an object which is not owned. A
common pattern is to use a C++ reference where a reference is mandatory,
and a C++ pointer where a reference is optional (using nullptr as the
"missing" state).

void foo(bar const& mandatory);
void foo(bar const* optional);

However, there are a few shortcomings to this approach:

1. It lacks semantics. The meaning of a raw pointer is overloaded; are
we passing a reference, an array, an "iterator", an "owning" pointer, or a
string (in the case of char const*)? Even if we know it is a reference,
is it really optional? Some people don't like representing references using
C++ references, so they used pointers that cannot be null instead. The GSL
is addressing this somewhat with the gsl::not_null annotation, but this
is hardly optimal. Speaking of references, they also have overloaded
meaning: passing by reference can be for performance, or because a
reference is genuinely needed (and maybe stored somewhere).
2. C++ references cannot be reassigned. I understand this is one of the
motivating forces behind the operator dot proposal. I agree it is a big
problem.

What we need are some wrapper types which can represent references
(mandatory or optional) to arbitrary objects. One proposal
<http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4282> already
exists which kind of addresses this need: std::observer_ptr (a.k.a. The
World's Dumbest Smart Pointer). This is nice because it conveys meaning (it
"observes"; it doesn't "own"), but it falls short in a few other areas.
Firstly, it doesn't support mandatory references (pointers can be null).
Secondly, it still has "pointer" semantics. I would like my smart
references to have "reference" semantics; in other words, I do not want to
have to enter the realm of C++ pointers; I do not want to have to do this:

std::observer_ptr<int> o = &i; // address-of operator: yuck!

The problem is that std::observer_ptr is an "observing" pointer modelled on
the "owning" smart pointers, hence why is is kind of dumb. I propose that
we need wrappers designed from the ground up to model some kind of
"reference" concept. However, since std::ref is already taken, and for
other reasons which should become apparent later, I suggest not using the
term "reference". We could follow std::observer_ptr and use the term
"observer", but I feel that the name "view" is more appropriate, having
been coined by std::string_view as a term for something which is a
non-owning reference to another object. Thus, I suggest std::view:

std::view<int> v = i; // no pointer semantics: yay!

But, I hear you say, don't we need the operator dot proposal to get our
"reference-y" semantics? Well, to get C++ reference semantics, yes we do.
However, it is not required. Consider this:

std::view<int> v = i;
v = j; // reassignment of the view
*v = 42; // assignment of the referenced object

This has the advantage that assignment of the view and assignment of the
referenced object are clearly disambiguated; there is no need to change the
language, and there is less potential for confusing. But, but, I hear you
protest, this is pointer semantics! Indeed, the operator dot proposal
mentions this as a motivating factor. However, there is already precedent
for non-pointer usage of operator* and operator-> in std::optional:

std::optional<int> o = i;
o = j; // reassignment of the optional
*o = 42; // assignment of the underlying object

Indeed, std::view would have the exact same semantics as std::optional.
Thus, we have a "smart reference" type which requires no change to the
language, has no null/missing state, supports reassignment, has non-pointer
semantics, and has usage patterns consistent with other types already in
the standard library. Optional references can also be implemented naturally:

std::optional_view<int> o; // can be default constructed
o = i; // reassignment of the optional
o = std::view<int>(j); // optional views can be assigned from views, but
not vice-versa

if (o) {
*o = 42; // assignment of the underlying object
}

o = std::nullopt; // reuse of existing library features: yay!

I think this is pretty neat. And none of this required support for
overloading operator dot. Indeed, I feel that the the ambiguous nature of
operator= for a type which overloads operator dot may be a red flag that
something is amiss. However, I'm sure this has been discussed at length,
and perhaps the operator dot proposal is orthogonal to what I am suggesting
here. I just wanted to show that we can already address a number of
concerns without adding complexity to the language, and in my opinion, far
more cleanly than any solution involving operator dot.

One final thought: would the existence of std::optional_view make
std::observer_ptr redundant? My feeling is "perhaps", but I wouldn't be
surprised the answer were "no".

Thanks for taking the time to read my musings. Please let me know your
opinions, and if anything like this has been suggested before.

Kind regards,

Joseph Thomson
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/41be9d41-d160-4be8-bd1c-a662e108211d%40isocpp.org.
D. B.
2016-10-11 06:07:29 UTC
Permalink
You said "I would like my smart references to have "reference" semantics;
in other words, I do not want to have to enter the realm of C++ pointers; I
do not want to have to do this:"

But both of your examples seem to require - and rationalise - such. What am
I missing? OK, so they're more pointer _linguistics_ than semantics, but
it's still pointer syntax.

Conversely, the point of the recent smart reference proposals, AFAICT -
other than making writing code a lot easier in supportive situations - is
that they could be transparently incorporated into generic code that would
work with plain reference.

To require pointer syntax I think defeats this and makes such proposals a
moot point, since we could implement our own pretty trivilly where required
- and indeed, often do. What operator dot et al propose is a way to save us
that work by adding required scaffolding to the language itself, whch makes
it moot.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CACGiwhFSd4an44mf30efNb-5_Q_1Ee6ct8q%2BQG54BEgqcLosdg%40mail.gmail.com.
j***@gmail.com
2016-10-11 06:54:44 UTC
Permalink
Post by D. B.
You said "I would like my smart references to have "reference" semantics;
in other words, I do not want to have to enter the realm of C++ pointers; I
do not want to have to do this:"
But both of your examples seem to require - and rationalise - such. What
am I missing? OK, so they're more pointer _linguistics_ than semantics, but
it's still pointer syntax.
Yes, operator* and operator-> are pointer syntax in that they are defined
for pointer types, but I provided std::optional as an example of a
non-pointer type in the standard library which uses these operators to show
that they have taken on new meaning. Perhaps in modern C++ they would be
better referred to as "wrapper syntax". When I said I wanted to avoid
pointer semantics, I meant that I didn't want people to think of the type
as a pointer (semantics = meaning). If you can assign from a pointer, it
suggests that the type in some way models a pointer.

std::view<int> v = &i; // not what I want (rhs is of type int*)
std::view<int> v = i; // what I want (rhs is of type int&)
Post by D. B.
Conversely, the point of the recent smart reference proposals, AFAICT -
other than making writing code a lot easier in supportive situations - is
that they could be transparently incorporated into generic code that would
work with plain reference.
I'm not sure what you mean by "supportive situations". And I would like to
see some real examples of where it is useful to be able to write generic
code using reference syntax (operator dot). I'm not denying such examples
exist, I just can't think of any myself. And it's possible to write generic
code that operates on all wrapper types.

template <typename T>
void call_foo_checked(T const& v) {
if (v) {
v->foo();
}
}

bar b;

call_foo_checked(std::make_unique<bar>(b));
call_foo_checked(std::make_shared<bar>(b));
call_foo_checked(std::make_optional(b));
call_foo_checked(std::make_view(b)); // operator bool returns true; check
will be optimized away
call_foo_checked(std::make_optional_view(b));
Post by D. B.
To require pointer syntax I think defeats this and makes such proposals a
moot point, since we could implement our own pretty trivilly where required
- and indeed, often do. What operator dot et al propose is a way to save us
that work by adding required scaffolding to the language itself, whch makes
it moot.
As I just explained, it's wrapper syntax, not just pointer syntax. And the
idea is not necessarily to avoid specific syntax; it's to model a concept
(in this case, views on objects) in a natural way that meshes well with the
rest of the language and standard library. I don't like the operator dot
proposal because it does not feel natural; it feels forced. That said, my
aim here is not to prevent the proposal from being accepted; I'm just
trying to provide an alternative when it comes to modelling the
reference/view concept.

And many features of the standard library could be trivially implemented;
there are reasons for including something in the standard library other
than "it's hard to implement".
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/ef844f2c-8a17-44d0-b1e9-9c6ff2cdbda7%40isocpp.org.
m***@gmail.com
2016-10-11 07:24:19 UTC
Permalink
The ironic part is - operator.() is proposed exactly because *o = 42 (o
being optional) is cumbersome.
In the dot proposal there is even an example of implementing optional on
top of the dot op!

In any case dot op is "enabling technology" - its uses exceed using it to
implement references, it is just that this particular proposal focuses way
too much on references.
There are other proposals which focus on other uses.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/01673d6d-0d1d-443c-9d7a-bfc487d3946c%40isocpp.org.
j***@gmail.com
2016-10-11 08:18:43 UTC
Permalink
Post by m***@gmail.com
The ironic part is - operator.() is proposed exactly because *o = 42 (o
being optional) is cumbersome.
In the dot proposal there is even an example of implementing optional on
top of the dot op!
I realize that this is why the proposal exists, hence why I called it
"syntactic sugar". I don't see how all this added complexity is worth it
just to avoid typing one additional character.
Post by m***@gmail.com
In any case dot op is "enabling technology" - its uses exceed using it to
implement references, it is just that this particular proposal focuses way
too much on references.
There are other proposals which focus on other uses.
My proposal isn't specifically about operator dot, and I mentioned that the
proposals may be orthogonal. My proposal focuses on references because it
is about references (in the general sense, not the C++ syntactic sense).
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/593b2e80-9f04-430e-86c6-ea7537424d96%40isocpp.org.
D. B.
2016-10-11 08:45:01 UTC
Permalink
But it's not. It's about handles with pointer syntax.

A smart reference would be something that could be used as-if it was the
referred object, but while applying other
checks/validation/proxying/younameit on top of it. It should be
substitutable into template code that expects normal references - and hence
never uses * or -> - without said code being any the wiser.

What you're proposing isn't a smart reference. It's an even dumber pointer.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CACGiwhHD4gvnXEXaAoSmjB9YbVF3Gg346Kq4CTvt-yHV2YMniA%40mail.gmail.com.
D. B.
2016-10-11 08:48:17 UTC
Permalink
Post by D. B.
Conversely, the point of the recent smart reference proposals, AFAICT -
Post by D. B.
other than making writing code a lot easier in supportive situations - is
that they could be transparently incorporated into generic code that would
work with plain reference.
I'm not sure what you mean by "supportive situations". And I would like to
see some real examples of where it is useful to be able to write generic
code using reference syntax (operator dot). I'm not denying such examples
exist, I just can't think of any myself. And it's possible to write generic
code that operates on all wrapper types.
I meant "relevant situations". Such as those where I currently have to
write my own handle/proxy classes, which in 99% of cases just
reimplement/forward countless trivial operators/methods. With a transparent
smart reference, myself and other programmers in analogous situations - who
I don't think are as uncommon as you imply - would not need to do that. We
would only override the operations where we needed special behaviour, and
let operator.(), or equivalent, forward anything that we didn't explicitly
'overridde'.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CACGiwhGs3zp9h_omF8qgwAzx3%2B07DrUcikQE7O4NFJJ-ASerkA%40mail.gmail.com.
j***@gmail.com
2016-10-11 09:17:46 UTC
Permalink
Post by D. B.
But it's not. It's about handles with pointer syntax.
I'm trying to draw a distinction between C++ references and references as a
general concept. References indirectly refer to objects. Both C++ pointers
and C++ references are forms of reference in the general sense. My proposed
types are handles that override operator* and operator->, so they are
handles with pointer syntax much like std::optional is a handle with
pointer syntax.

A smart reference would be something that could be used as-if it was the
Post by D. B.
referred object, but while applying other
checks/validation/proxying/younameit on top of it. It should be
substitutable into template code that expects normal references - and hence
never uses * or -> - without said code being any the wiser.
Okay, then I don't claim that what I'm proposing are smart references. What
I'm proposing are views, as per my chosen type names, which are a kind of
reference type in the general sense.
Post by D. B.
What you're proposing isn't a smart reference. It's an even dumber pointer.
It's not a pointer because it doesn't have pointer semantics. But it is
just about as dumb as std::observer_ptr, hence why I drew the comparison. I
wouldn't say it's any dumber though.

I meant "relevant situations". Such as those where I currently have to
Post by D. B.
write my own handle/proxy classes, which in 99% of cases just
reimplement/forward countless trivial operators/methods. With a transparent
smart reference, myself and other programmers in analogous situations - who
I don't think are as uncommon as you imply - would not need to do that. We
would only override the operations where we needed special behaviour, and
let operator.(), or equivalent, forward anything that we didn't explicitly
'overridde'.
Yes, this is the compile-time overriding that I mentioned in the opening
paragraph of my first post. This is not the problem I am trying to solve.
As I said, I am not trying to prevent the operator dot proposal being
accepted, even though I don't like it very much. I am just presenting an
alternative solution for one particular use case of operator dot
overloading.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/51343022-120f-40a7-bb01-92ee19e7a01e%40isocpp.org.
D. B.
2016-10-11 09:24:33 UTC
Permalink
Cool, I guess I focused in too much on the comparison to operator dot and
mistakenly inferred this was intended as a direct rival for that, rather
than a complimentary alternative. In that case, carry on - and I'll reread
it all properly now. :)
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CACGiwhGzGGZSo2A-ni1H9xRvjsgR5CmYdBKq8ZE8ftO4J7ZsaA%40mail.gmail.com.
j***@gmail.com
2016-10-11 09:46:45 UTC
Permalink
Sorry. It's my fault! My intention was to show that operator dot wasn't a
necessary part of the solution to this problem, but I ended up focusing on
it too much.
Post by D. B.
Cool, I guess I focused in too much on the comparison to operator dot and
mistakenly inferred this was intended as a direct rival for that, rather
than a complimentary alternative. In that case, carry on - and I'll reread
it all properly now. :)
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/43b87b7e-d623-418d-b1ff-6d20ed5fae92%40isocpp.org.
m***@gmail.com
2016-10-11 11:46:37 UTC
Permalink
The problem is, it's kind of too late, because there is very strong
determination to get some (sort of) dot overloading into the standard.
It will not only enable smart refs/views, but other scenarios as well.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/46c4022e-720c-45fb-823d-751c6d8af365%40isocpp.org.
j***@gmail.com
2016-10-12 00:03:20 UTC
Permalink
As I said, I am not making a case against operator dot overloading. My
mistake was conflating C++ references and references in the general sense
(what I am calling "views"). Smart references are types that mimic C++
references. "Views" are types that model non-owning references in the
general sense. I believe this proposal is orthogonal to any operator dot
overloading proposal, as not all "references" have to look like C++
references.
Post by m***@gmail.com
The problem is, it's kind of too late, because there is very strong
determination to get some (sort of) dot overloading into the standard.
It will not only enable smart refs/views, but other scenarios as well.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/e81406c7-a398-4851-97b2-638d74312e66%40isocpp.org.
Nicol Bolas
2016-10-12 00:19:53 UTC
Permalink
Post by j***@gmail.com
As I said, I am not making a case against operator dot overloading. My
mistake was conflating C++ references and references in the general sense
(what I am calling "views"). Smart references are types that mimic C++
references. "Views" are types that model non-owning references in the
general sense. I believe this proposal is orthogonal to any operator dot
overloading proposal, as not all "references" have to look like C++
references.
I just don't see the point of a generalized `view` and your `optional_view`.

`view<T>` is just `not_null<T*>`; `optional_view<T>` is just `T*`. If you
don't like the pointer semantics of it... tough.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/8ebe632f-9293-421e-ad10-5e0c5c485ccd%40isocpp.org.
j***@gmail.com
2016-10-12 01:09:20 UTC
Permalink
I don't like the pointer semantics of not_null. Unnatural semantics hint at
incorrect design. not_null is great for making existing and legacy code
safer, but it is not the ideal design for a "view" type.

One of the main differences between not_null and view is that not_null
detects null pointer errors at run time, while view catches them at compile
time.

void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
Post by Nicol Bolas
Post by j***@gmail.com
As I said, I am not making a case against operator dot overloading. My
mistake was conflating C++ references and references in the general sense
(what I am calling "views"). Smart references are types that mimic C++
references. "Views" are types that model non-owning references in the
general sense. I believe this proposal is orthogonal to any operator dot
overloading proposal, as not all "references" have to look like C++
references.
I just don't see the point of a generalized `view` and your
`optional_view`.
`view<T>` is just `not_null<T*>`; `optional_view<T>` is just `T*`. If you
don't like the pointer semantics of it... tough.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/2ee3050d-ee55-42ed-b1ee-16503ea5296b%40isocpp.org.
j***@gmail.com
2016-10-12 01:24:06 UTC
Permalink
And yes, I know that the potential error of an assignment of int* to
not_null<int*> can be caught by a static analysis tool, but A) this
requires a static analysis tool, and B) not_null and view are
complementary. Let me rewrite my example:

void foo(not_null<int*> p) { // legacy interface made safer with not_null
view<int> v = *p; // modern implementation (static analysis: a-okay)
...
}
Post by j***@gmail.com
I don't like the pointer semantics of not_null. Unnatural semantics hint
at incorrect design. not_null is great for making existing and legacy
code safer, but it is not the ideal design for a "view" type.
One of the main differences between not_null and view is that not_null
detects null pointer errors at run time, while view catches them at
compile time.
void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/041b8772-4d46-4faa-bc06-d6da741b620d%40isocpp.org.
Nicol Bolas
2016-10-12 02:06:11 UTC
Permalink
Post by j***@gmail.com
Post by j***@gmail.com
I don't like the pointer semantics of not_null. Unnatural semantics hint
at incorrect design. not_null is great for making existing and legacy
code safer, but it is not the ideal design for a "view" type.
One of the main differences between not_null and view is that not_null
detects null pointer errors at run time, while view catches them at
compile time.
void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
That's not a NULL pointer check. Nothing ever detects that `p` is a null
pointer. It fails to compile only because `view<int>` cannot be constructed
from a pointer at all. If you had done `view<int> = *p`, it would compile
just fine. What it would not do is actually catch an error if you pass
`nullptr` to `foo`. It would simply invoke UB.

And yes, I know that the potential error of an assignment of int* to
Post by j***@gmail.com
not_null<int*> can be caught by a static analysis tool, but A) this
requires a static analysis tool, and B) not_null and view are
void foo(not_null<int*> p) { // legacy interface made safer with not_null
view<int> v = *p; // modern implementation (static analysis: a-okay)
...
}
But why would you use `view<int>` inside of `foo` at all? There would be no
objective *need*, unless you're just allergic to using `->`.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/142ffa44-8b33-49a6-888a-0a1a78ae6a83%40isocpp.org.
Nicol Bolas
2016-10-12 02:08:15 UTC
Permalink
Post by Nicol Bolas
Post by j***@gmail.com
And yes, I know that the potential error of an assignment of int* to
not_null<int*> can be caught by a static analysis tool, but A) this
requires a static analysis tool, and B) not_null and view are
void foo(not_null<int*> p) { // legacy interface made safer with not_null
view<int> v = *p; // modern implementation (static analysis: a-okay)
...
}
But why would you use `view<int>` inside of `foo` at all? There would be
no objective *need*, unless you're just allergic to using `->`.
Let me rephrase that bit at the end.

`view<int>` and `not_null<int*>` have almost exactly the same interface.
The *only difference* between them is that one takes references and the
other takes pointers.

That trivial difference is not worth creating a whole new type for.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/15f04dae-8f9c-468a-a2ab-800ce1305e24%40isocpp.org.
j***@gmail.com
2016-10-12 03:54:27 UTC
Permalink
void foo(not_null<int*> p) { // legacy interface made safer with not_null
Post by j***@gmail.com
view<int> v = *p; // modern implementation (static analysis: a-okay)
Post by Nicol Bolas
Post by j***@gmail.com
...
}
But why would you use `view<int>` inside of `foo` at all? There would be
no objective *need*, unless you're just allergic to using `->`.
It's just a contrived example. I will try to give more practical examples
from now on (and I assume given your follow-up, you noticed that view does
require operator->).

One of the main differences between not_null and view is that not_null
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
detects null pointer errors at run time, while view catches them at
compile time.
void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
That's not a NULL pointer check. Nothing ever detects that `p` is a null
pointer. It fails to compile only because `view<int>` cannot be constructed
from a pointer at all. If you had done `view<int> = *p`, it would compile
just fine. What it would not do is actually catch an error if you pass
`nullptr` to `foo`. It would simply invoke UB.
This was part of the motivation for my second example. Correct usage would
be something like this (disclaimer: the example is still somewhat
contrived). Given some class, watcher:

class watcher {
std::vector<view<item const>> watched_items;
...
};

We might implement the method, watch:

void watch(item const* i) {
if (i) {
watched_items.push_back(*i);
} else {
throw std::runtime_error("i cannot be null");
}
}

Or of course, we can use not_null to push the exception further up the call
stack:

void watch(not_null<item const*> i) {
watched_items.push_back(*i);
}

Or, if we can modify the interface, we can make the caller responsible for
dereferencing their pointer:

void watch(item const& i) {
watched_items.push_back(i);
}

Or maybe even:

void foo(view<item const> i) {
watched_items.push_back(i);
}

A note about the last two examples: syntactically they are identical, but
semantically they are not. Use of view implies that the "reference" may be
stored by the class; use of reference to const is often done just for
performance reasons (to avoid making an expensive copy).

Let me rephrase that bit at the end.
Post by j***@gmail.com
`view<int>` and `not_null<int*>` have almost exactly the same interface.
The *only difference* between them is that one takes references and the
other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors into
compile time errors and pushes those errors up the call stack. I think this
is important. I've already listed one other reason: semantics. view can
convey meaning. As for other features, it really is just general syntactic
niceness. The design of view and optional_view are still somewhat in flux,
but I can think of at least one other important feature.

I currently have it so that view<T> is implicitly convertible to T&. This
allows you to do things like this:

void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}

This could be generic code that operates on both containers of T and
containers of view<T>. Another (contrived) example of this feature in use:

std::array<int, 3> i = { 0, 1, 2 };
std::map<std::string, optional_view<int>> m;

m["a"] = i[0];
m["b"] = i[1];
m["c"] = i[2];

int& b = m["b"];

Here I used optional_view because it is default constructible. I could have
used view if I had used map::emplace and map::at.

Note however that view<T> is not convertible to T. This is because I want
symmetry and consistency. view is not a value type, so I do not want it to
be able to implicitly convert to and from other value types. You have to
"dereference" it to access the underlying value. optional_view<T> also
implicitly converts to T&, but throws an exception if it is empty.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/dd418983-c40b-4deb-8cff-e004547cc75a%40isocpp.org.
Nicol Bolas
2016-10-12 06:17:19 UTC
Permalink
Post by j***@gmail.com
void foo(not_null<int*> p) { // legacy interface made safer with not_null
Post by j***@gmail.com
view<int> v = *p; // modern implementation (static analysis: a-okay)
Post by Nicol Bolas
Post by j***@gmail.com
...
}
But why would you use `view<int>` inside of `foo` at all? There would be
no objective *need*, unless you're just allergic to using `->`.
It's just a contrived example. I will try to give more practical examples
from now on (and I assume given your follow-up, you noticed that view does
require operator->).
One of the main differences between not_null and view is that not_null
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
detects null pointer errors at run time, while view catches them at
compile time.
void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
That's not a NULL pointer check. Nothing ever detects that `p` is a null
pointer. It fails to compile only because `view<int>` cannot be constructed
from a pointer at all. If you had done `view<int> = *p`, it would compile
just fine. What it would not do is actually catch an error if you pass
`nullptr` to `foo`. It would simply invoke UB.
This was part of the motivation for my second example. Correct usage would
be something like this (disclaimer: the example is still somewhat
class watcher {
std::vector<view<item const>> watched_items;
...
};
void watch(item const* i) {
if (i) {
watched_items.push_back(*i);
} else {
throw std::runtime_error("i cannot be null");
}
}
Or of course, we can use not_null to push the exception further up the
void watch(not_null<item const*> i) {
watched_items.push_back(*i);
}
Or, if we can modify the interface, we can make the caller responsible for
void watch(item const& i) {
watched_items.push_back(i);
}
void foo(view<item const> i) {
watched_items.push_back(i);
}
A note about the last two examples: syntactically they are identical, but
Post by j***@gmail.com
semantically they are not. Use of view implies that the "reference" may be
stored by the class; use of reference to const is often done just for
performance reasons (to avoid making an expensive copy).
The use of a view implies no such thing. Or at least, not in the way
"views" get used in other contexts. `string_view` for example very much *does
not* have this implication. After all, `basic_string` is *implicitly*
convertible to a `string_view`, even from a string temporary. So any code
that takes a `string_view` as a parameter and tries to store it longer-term
is in for a rude awakening.

Why should `view<T>` have different semantics in this regard? It certainly
is not how users would expect it to work.

In any case, it is highly dangerous to store any pointer/reference to an
object in a class without an explicit ownership contract. Not unless you
have special knowledge of the source. And we have a way to represent that
"special knowledge" already. Or more importantly, that the function will
explicitly have some expectation of the object surviving the call stack.

We call that `gsl::owner<T>`. If you wrap your parameter in that, then it
expresses the idea that the function claims some form of ownership of the
object.
Post by j***@gmail.com
Let me rephrase that bit at the end.
Post by j***@gmail.com
`view<int>` and `not_null<int*>` have almost exactly the same interface.
The *only difference* between them is that one takes references and the
other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors into
compile time errors and pushes those errors up the call stack.
That they push errors up the call stack is true, but just as with
`not_null`, it only does so to the degree that the caller uses the type in
question directly. What `view<T>` doesn't do is actually cause runtime
checks for the error. So if you have a `T*`, and you don't check if it's
NULL, turning it into a `view<T>` is no less dangerous than turning it into
a `not_null<T*>`. Indeed, it's more dangerous, since `not_null` will throw,
while `view<T>` will have no idea that it has a NULL reference.

I have yet to see an example where a runtime error becomes a compile-time
error as you suggest. This code is a potential runtime error:

void foo(T *t)
{
not_null<T*> nnt = t;
}

Show me the equivalent `view<T>` code that makes this a compile-time error.
But *only when `t` is NULL*. That is, this code should compile fine for any
user who calls `foo` with a not-NULL pointer. But should fail to compile
when `foo` is called with `nullptr`. That's what making "run time errors
into compile time errors" would be; after all, the above code only issues a
runtime error if you *actually call it* with NULL.

If the compile-time version can't compile-time check the pointer's value,
then it's not doing the same work. And therefore, it is not equivalent
code; it'd an apples-to-oranges comparison.

I think this is important. I've already listed one other reason: semantics.
Post by j***@gmail.com
view can convey meaning. As for other features, it really is just general
syntactic niceness. The design of view and optional_view are still
somewhat in flux, but I can think of at least one other important feature.
I currently have it so that view<T> is implicitly convertible to T&. This
void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}
This could be generic code that operates on both containers of T and
containers of view<T>.
Generic in what way? Your generic code couldn't actually *do anything* with
the `view<T>`. Not directly. It could pass it to some other functions, to
be sure. But it could only pass it to ones which took the `view<T>`. This
function itself cannot access members of the object directly. It couldn't
send it to any function that took a `const T&`. And so forth.

The only thing that would allow "generic code" to work as you suggest is
operator-dot.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/390cbd81-8e95-4932-9cca-ac58a008f7ec%40isocpp.org.
j***@gmail.com
2016-10-12 07:07:15 UTC
Permalink
Post by j***@gmail.com
Post by j***@gmail.com
void foo(not_null<int*> p) { // legacy interface made safer with not_null
Post by j***@gmail.com
view<int> v = *p; // modern implementation (static analysis: a-okay)
Post by Nicol Bolas
Post by j***@gmail.com
...
}
But why would you use `view<int>` inside of `foo` at all? There would
be no objective *need*, unless you're just allergic to using `->`.
It's just a contrived example. I will try to give more practical examples
from now on (and I assume given your follow-up, you noticed that view does
require operator->).
One of the main differences between not_null and view is that not_null
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
detects null pointer errors at run time, while view catches them at
compile time.
void foo(int* p) {
not_null<int*> nn = p; // run-time exception
view<int> = p; // compile-time error
}
That's not a NULL pointer check. Nothing ever detects that `p` is a null
pointer. It fails to compile only because `view<int>` cannot be constructed
from a pointer at all. If you had done `view<int> = *p`, it would compile
just fine. What it would not do is actually catch an error if you pass
`nullptr` to `foo`. It would simply invoke UB.
This was part of the motivation for my second example. Correct usage
would be something like this (disclaimer: the example is still somewhat
class watcher {
std::vector<view<item const>> watched_items;
...
};
void watch(item const* i) {
if (i) {
watched_items.push_back(*i);
} else {
throw std::runtime_error("i cannot be null");
}
}
Or of course, we can use not_null to push the exception further up the
void watch(not_null<item const*> i) {
watched_items.push_back(*i);
}
Or, if we can modify the interface, we can make the caller responsible
void watch(item const& i) {
watched_items.push_back(i);
}
void foo(view<item const> i) {
watched_items.push_back(i);
}
A note about the last two examples: syntactically they are identical, but
Post by j***@gmail.com
semantically they are not. Use of view implies that the "reference" may be
stored by the class; use of reference to const is often done just for
performance reasons (to avoid making an expensive copy).
The use of a view implies no such thing. Or at least, not in the way
"views" get used in other contexts. `string_view` for example very much *does
not* have this implication. After all, `basic_string` is *implicitly*
convertible to a `string_view`, even from a string temporary. So any code
that takes a `string_view` as a parameter and tries to store it longer-term
is in for a rude awakening.
Why should `view<T>` have different semantics in this regard? It certainly
is not how users would expect it to work.
In any case, it is highly dangerous to store any pointer/reference to an
object in a class without an explicit ownership contract. Not unless you
have special knowledge of the source. And we have a way to represent that
"special knowledge" already. Or more importantly, that the function will
explicitly have some expectation of the object surviving the call stack.
We call that `gsl::owner<T>`. If you wrap your parameter in that, then it
expresses the idea that the function claims some form of ownership of the
object.
In my example I was obviously assuming special knowledge, as it is indeed
dangerous to store random non-owning references to things. You are right
that use of view doesn't imply long-term storage; that information has to
be conveyed in the API documentation. I was mistaken.

Let me rephrase that bit at the end.
Post by j***@gmail.com
Post by j***@gmail.com
Post by j***@gmail.com
`view<int>` and `not_null<int*>` have almost exactly the same interface.
The *only difference* between them is that one takes references and the
other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors into
compile time errors and pushes those errors up the call stack.
That they push errors up the call stack is true, but just as with
`not_null`, it only does so to the degree that the caller uses the type in
question directly. What `view<T>` doesn't do is actually cause runtime
checks for the error. So if you have a `T*`, and you don't check if it's
NULL, turning it into a `view<T>` is no less dangerous than turning it into
a `not_null<T*>`. Indeed, it's more dangerous, since `not_null` will throw,
while `view<T>` will have no idea that it has a NULL reference.
I have yet to see an example where a runtime error becomes a compile-time
void foo(T *t)
{
not_null<T*> nnt = t;
}
Show me the equivalent `view<T>` code that makes this a compile-time
error. But *only when `t` is NULL*. That is, this code should compile
fine for any user who calls `foo` with a not-NULL pointer. But should fail
to compile when `foo` is called with `nullptr`. That's what making "run
time errors into compile time errors" would be; after all, the above code
only issues a runtime error if you *actually call it* with NULL.
If the compile-time version can't compile-time check the pointer's value,
then it's not doing the same work. And therefore, it is not equivalent
code; it'd an apples-to-oranges comparison.
What I mean is that the API of not_null allows for run time errors, while
the API of view doesn't. If you want zero-overhead code, it is desirable to
eliminate run time error checking. When I say it turns a run time error
into a compile time error, I mean it does that by pushing the error up the
stack.
Post by j***@gmail.com
Post by j***@gmail.com
semantics. view can convey meaning. As for other features, it really is
just general syntactic niceness. The design of view and optional_view
are still somewhat in flux, but I can think of at least one other important
feature.
I currently have it so that view<T> is implicitly convertible to T&.
void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}
This could be generic code that operates on both containers of T and
containers of view<T>.
Generic in what way? Your generic code couldn't actually *do anything*
with the `view<T>`. Not directly. It could pass it to some other functions,
to be sure. But it could only pass it to ones which took the `view<T>`.
This function itself cannot access members of the object directly. It
couldn't send it to any function that took a `const T&`. And so forth.
The only thing that would allow "generic code" to work as you suggest is
operator-dot.
I meant that the code could operate on both containers of T and containers
of view<T>., because view<T> can be converted to T&. This was just a
passing comment though; I wasn't trying to make a genuine case about view
helping to write generic code. Incidentally though, could a proper "smart
reference" which overloaded operator dot be used here? My understanding was
that operator= would apply to the wrapped object (unless it were
implemented in the wrapper, which would break its ref-like behaviour), so
the reference itself wouldn't be copy assignable, which would prohibit
storing it in a std::vector. Genuine question.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/ff9f439a-06fd-472b-8261-2c6f648042f5%40isocpp.org.
Nicol Bolas
2016-10-12 13:43:23 UTC
Permalink
Post by Nicol Bolas
Let me rephrase that bit at the end.
Post by Nicol Bolas
Post by j***@gmail.com
Post by Nicol Bolas
`view<int>` and `not_null<int*>` have almost exactly the same
interface. The *only difference* between them is that one takes
references and the other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors into
compile time errors and pushes those errors up the call stack.
That they push errors up the call stack is true, but just as with
`not_null`, it only does so to the degree that the caller uses the type in
question directly. What `view<T>` doesn't do is actually cause runtime
checks for the error. So if you have a `T*`, and you don't check if it's
NULL, turning it into a `view<T>` is no less dangerous than turning it into
a `not_null<T*>`. Indeed, it's more dangerous, since `not_null` will throw,
while `view<T>` will have no idea that it has a NULL reference.
I have yet to see an example where a runtime error becomes a compile-time
void foo(T *t)
{
not_null<T*> nnt = t;
}
Show me the equivalent `view<T>` code that makes this a compile-time
error. But *only when `t` is NULL*. That is, this code should compile
fine for any user who calls `foo` with a not-NULL pointer. But should fail
to compile when `foo` is called with `nullptr`. That's what making "run
time errors into compile time errors" would be; after all, the above code
only issues a runtime error if you *actually call it* with NULL.
If the compile-time version can't compile-time check the pointer's value,
then it's not doing the same work. And therefore, it is not equivalent
code; it'd an apples-to-oranges comparison.
What I mean is that the API of not_null allows for run time errors, while
the API of view doesn't. If you want zero-overhead code, it is desirable
to eliminate run time error checking. When I say it turns a run time error
into a compile time error, I mean it does that by pushing the error up the
stack.
Yes. And by doing so, it has pushed it up the stack to the point where it
doesn't actually *check* the error. If the user has a pointer, and they
want to use `view`, it is *the user* who has to test if it isn't NULL.
Whereas `not_null` does the check automatically.

So the above code is safer than this:

void foo(T *t)
{
view<T> vt = *t; //If null, UB, but nobody checks.
}

Pushing errors "up the stack" is only a good thing if you actually check
for them.
Post by Nicol Bolas
Post by Nicol Bolas
Post by j***@gmail.com
semantics. view can convey meaning. As for other features, it really is
just general syntactic niceness. The design of view and optional_view
are still somewhat in flux, but I can think of at least one other important
feature.
I currently have it so that view<T> is implicitly convertible to T&.
void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}
This could be generic code that operates on both containers of T and
containers of view<T>.
Generic in what way? Your generic code couldn't actually *do anything*
with the `view<T>`. Not directly. It could pass it to some other functions,
to be sure. But it could only pass it to ones which took the `view<T>`.
This function itself cannot access members of the object directly. It
couldn't send it to any function that took a `const T&`. And so forth.
The only thing that would allow "generic code" to work as you suggest is
operator-dot.
I meant that the code could operate on both containers of T and
containers of view<T>., because view<T> can be converted to T&. This was
just a passing comment though; I wasn't trying to make a genuine case about
view helping to write generic code. Incidentally though, could a proper
"smart reference" which overloaded operator dot be used here? My
understanding was that operator= would apply to the wrapped object
(unless it were implemented in the wrapper, which would break its ref-like
behaviour), so the reference itself wouldn't be copy assignable, which
would prohibit storing it in a std::vector. Genuine question.
With operator-dot, attempting to call `operator=(const smart_ref<T>&)`
would apply to the smart reference. Attempts to use `operator=(const T&)`
would be forwarded to `T`. So smart references could be copy-assignable if
you want, without breaking the ability to treat them as references.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/618deb4f-3912-4f97-9c62-6b3bec858272%40isocpp.org.
j***@gmail.com
2016-10-12 15:47:20 UTC
Permalink
Post by Nicol Bolas
Post by Nicol Bolas
Let me rephrase that bit at the end.
Post by Nicol Bolas
Post by j***@gmail.com
Post by Nicol Bolas
`view<int>` and `not_null<int*>` have almost exactly the same
interface. The *only difference* between them is that one takes
references and the other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors
into compile time errors and pushes those errors up the call stack.
That they push errors up the call stack is true, but just as with
`not_null`, it only does so to the degree that the caller uses the type in
question directly. What `view<T>` doesn't do is actually cause runtime
checks for the error. So if you have a `T*`, and you don't check if it's
NULL, turning it into a `view<T>` is no less dangerous than turning it into
a `not_null<T*>`. Indeed, it's more dangerous, since `not_null` will throw,
while `view<T>` will have no idea that it has a NULL reference.
I have yet to see an example where a runtime error becomes a
void foo(T *t)
{
not_null<T*> nnt = t;
}
Show me the equivalent `view<T>` code that makes this a compile-time
error. But *only when `t` is NULL*. That is, this code should compile
fine for any user who calls `foo` with a not-NULL pointer. But should fail
to compile when `foo` is called with `nullptr`. That's what making "run
time errors into compile time errors" would be; after all, the above code
only issues a runtime error if you *actually call it* with NULL.
If the compile-time version can't compile-time check the pointer's
value, then it's not doing the same work. And therefore, it is not
equivalent code; it'd an apples-to-oranges comparison.
What I mean is that the API of not_null allows for run time errors,
while the API of view doesn't. If you want zero-overhead code, it is
desirable to eliminate run time error checking. When I say it turns a run
time error into a compile time error, I mean it does that by pushing the
error up the stack.
Yes. And by doing so, it has pushed it up the stack to the point where it
doesn't actually *check* the error. If the user has a pointer, and they
want to use `view`, it is *the user* who has to test if it isn't NULL.
Whereas `not_null` does the check automatically.
void foo(T *t)
{
view<T> vt = *t; //If null, UB, but nobody checks.
}
Pushing errors "up the stack" is only a good thing if you actually check
for them.
That's my point: it's the user's responsibility to check that the pointer
isn't null before dereferencing it. Sure, not_null turns potential UB into
an exception, but at a performance cost (a run time check, and possible
overhead relating to exceptions). By using not_null in our interface, we
impose a performance cost (however small) on the user whether or not it's
even possible for them to pass a null pointer. Using view (or a plain
reference) allows our interface to have zero run time overhead; we just
have to trust the user not to dereference any null pointers they may have
hanging around. But ultimately, even if we use not_null, the user can still
dereference null pointers all day long. If we wanted to eliminate this
possibility, we would be better off encouraging the use of an alternative
to pointers which had no null state -- perhaps something like view :)
Post by Nicol Bolas
Post by Nicol Bolas
Post by Nicol Bolas
Post by j***@gmail.com
semantics. view can convey meaning. As for other features, it really is
just general syntactic niceness. The design of view and optional_view
are still somewhat in flux, but I can think of at least one other important
feature.
I currently have it so that view<T> is implicitly convertible to T&.
void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}
This could be generic code that operates on both containers of T and
containers of view<T>.
Generic in what way? Your generic code couldn't actually *do anything*
with the `view<T>`. Not directly. It could pass it to some other functions,
to be sure. But it could only pass it to ones which took the `view<T>`.
This function itself cannot access members of the object directly. It
couldn't send it to any function that took a `const T&`. And so forth.
The only thing that would allow "generic code" to work as you suggest is
operator-dot.
I meant that the code could operate on both containers of T and
containers of view<T>., because view<T> can be converted to T&. This was
just a passing comment though; I wasn't trying to make a genuine case about
view helping to write generic code. Incidentally though, could a proper
"smart reference" which overloaded operator dot be used here? My
understanding was that operator= would apply to the wrapped object
(unless it were implemented in the wrapper, which would break its ref-like
behaviour), so the reference itself wouldn't be copy assignable, which
would prohibit storing it in a std::vector. Genuine question.
With operator-dot, attempting to call `operator=(const smart_ref<T>&)`
would apply to the smart reference. Attempts to use `operator=(const T&)`
would be forwarded to `T`. So smart references could be copy-assignable if
you want, without breaking the ability to treat them as references.
Really? In that case, they can be stored in containers, but they don't
really behave 100% like references.

int a = 0;
int b = 0;

int& ra = a;
int& rb = b;

ra = rb; // copies referenced value

smart_ref<int> sra = a;
smart_ref<int> srb = b;

sra = srb; // rebinds reference

I thought the proposal suggested using a special "rebind" function so that
smart references behave exactly like regular references?
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/887a0fb4-f574-4c74-9d65-097fb008195c%40isocpp.org.
Nicol Bolas
2016-10-12 18:31:05 UTC
Permalink
Post by j***@gmail.com
Post by Nicol Bolas
Post by Nicol Bolas
Let me rephrase that bit at the end.
Post by Nicol Bolas
Post by j***@gmail.com
Post by Nicol Bolas
`view<int>` and `not_null<int*>` have almost exactly the same
interface. The *only difference* between them is that one takes
references and the other takes pointers.
That trivial difference is not worth creating a whole new type for.
As I hope I've demonstrated, taking a pointer makes run time errors
into compile time errors and pushes those errors up the call stack.
That they push errors up the call stack is true, but just as with
`not_null`, it only does so to the degree that the caller uses the type in
question directly. What `view<T>` doesn't do is actually cause runtime
checks for the error. So if you have a `T*`, and you don't check if it's
NULL, turning it into a `view<T>` is no less dangerous than turning it into
a `not_null<T*>`. Indeed, it's more dangerous, since `not_null` will throw,
while `view<T>` will have no idea that it has a NULL reference.
I have yet to see an example where a runtime error becomes a
void foo(T *t)
{
not_null<T*> nnt = t;
}
Show me the equivalent `view<T>` code that makes this a compile-time
error. But *only when `t` is NULL*. That is, this code should compile
fine for any user who calls `foo` with a not-NULL pointer. But should fail
to compile when `foo` is called with `nullptr`. That's what making "run
time errors into compile time errors" would be; after all, the above code
only issues a runtime error if you *actually call it* with NULL.
If the compile-time version can't compile-time check the pointer's
value, then it's not doing the same work. And therefore, it is not
equivalent code; it'd an apples-to-oranges comparison.
What I mean is that the API of not_null allows for run time errors,
while the API of view doesn't. If you want zero-overhead code, it is
desirable to eliminate run time error checking. When I say it turns a run
time error into a compile time error, I mean it does that by pushing the
error up the stack.
Yes. And by doing so, it has pushed it up the stack to the point where it
doesn't actually *check* the error. If the user has a pointer, and they
want to use `view`, it is *the user* who has to test if it isn't NULL.
Whereas `not_null` does the check automatically.
void foo(T *t)
{
view<T> vt = *t; //If null, UB, but nobody checks.
}
Pushing errors "up the stack" is only a good thing if you actually check
for them.
That's my point: it's the user's responsibility to check that the pointer
isn't null before dereferencing it. Sure, not_null turns potential UB
into an exception, but at a performance cost (a run time check, and
possible overhead relating to exceptions). By using not_null in our
interface, we impose a performance cost (however small) on the user whether
or not it's even possible for them to pass a null pointer. Using view (or
a plain reference) allows our interface to have zero run time overhead; we
just have to trust the user not to dereference any null pointers they may
have hanging around. But ultimately, even if we use not_null, the user
can still dereference null pointers all day long. If we wanted to eliminate
this possibility, we would be better off encouraging the use of an
alternative to pointers which had no null state -- perhaps something like
view :)
I admit that `not_null<T*>` would be better if it could be constructed from
a `T&`, which would also cause it to not bother checking if it is a null
reference. And I have made such a suggestion
<https://github.com/Microsoft/GSL/issues/396>.

But regardless, I don't really understand what you're getting at here.

OK, you have some function that takes a pointer. And your function
implicitly requires that this pointer not be NULL; therefore, it isn't
going to check to see if it's NULL or not. And you intend to store this
not-null pointer around for a time and use it later.

Then why do you not simply *store a pointer*? Why do you need `view<T>`
instead of `T*`? Your code already assumes it's not NULL; what do you gain
from using this type instead of `T*`? That you initialize it with `T&`
rather than `T*`? You still have to use `*` and `->` to access the `T`.
Post by j***@gmail.com
Post by Nicol Bolas
Post by Nicol Bolas
Post by Nicol Bolas
Post by j***@gmail.com
semantics. view can convey meaning. As for other features, it really is
just general syntactic niceness. The design of view and optional_view
are still somewhat in flux, but I can think of at least one other important
feature.
I currently have it so that view<T> is implicitly convertible to T&.
void monitor_items() {
for (item const& i : watched_items) {
monitor(i);
}
}
This could be generic code that operates on both containers of T and
containers of view<T>.
Generic in what way? Your generic code couldn't actually *do anything*
with the `view<T>`. Not directly. It could pass it to some other functions,
to be sure. But it could only pass it to ones which took the `view<T>`.
This function itself cannot access members of the object directly. It
couldn't send it to any function that took a `const T&`. And so forth.
The only thing that would allow "generic code" to work as you suggest
is operator-dot.
I meant that the code could operate on both containers of T and
containers of view<T>., because view<T> can be converted to T&. This
was just a passing comment though; I wasn't trying to make a genuine case
about view helping to write generic code. Incidentally though, could a
proper "smart reference" which overloaded operator dot be used here? My
understanding was that operator= would apply to the wrapped object
(unless it were implemented in the wrapper, which would break its ref-like
behaviour), so the reference itself wouldn't be copy assignable, which
would prohibit storing it in a std::vector. Genuine question.
With operator-dot, attempting to call `operator=(const smart_ref<T>&)`
would apply to the smart reference. Attempts to use `operator=(const T&)`
would be forwarded to `T`. So smart references could be copy-assignable if
you want, without breaking the ability to treat them as references.
Really? In that case, they can be stored in containers, but they don't
really behave 100% like references.
int a = 0;
int b = 0;
int& ra = a;
int& rb = b;
ra = rb; // copies referenced value
smart_ref<int> sra = a;
smart_ref<int> srb = b;
sra = srb; // rebinds reference
I thought the proposal suggested using a special "rebind" function so that
smart references behave exactly like regular references?
And if you want your smart references to work that way, simply declare the
assignment operator `= delete`. But the operator-dot proposal doesn't exist
to tell you what to do with your types; it tells you what you *can do* with
them.

The proposal for operator-dot defines a mechanism, not a policy on how you
build smart references. I imagine that many of them will `=delete` the
operator; maybe all of them. But that is not a question for the
`operator-dot` proposal; it's a question for proposals for actual smart
reference types.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/b8201a64-2101-48e3-b625-df547609ed99%40isocpp.org.
j***@gmail.com
2016-10-13 08:27:50 UTC
Permalink
Post by j***@gmail.com
That's my point: it's the user's responsibility to check that the pointer
Post by j***@gmail.com
isn't null before dereferencing it. Sure, not_null turns potential UB
into an exception, but at a performance cost (a run time check, and
possible overhead relating to exceptions). By using not_null in our
interface, we impose a performance cost (however small) on the user whether
or not it's even possible for them to pass a null pointer. Using view
(or a plain reference) allows our interface to have zero run time overhead;
we just have to trust the user not to dereference any null pointers they
may have hanging around. But ultimately, even if we use not_null, the
user can still dereference null pointers all day long. If we wanted to
eliminate this possibility, we would be better off encouraging the use of
an alternative to pointers which had no null state -- perhaps something
like view :)
I admit that `not_null<T*>` would be better if it could be constructed
from a `T&`, which would also cause it to not bother checking if it is a
null reference. And I have made such a suggestion
<https://github.com/Microsoft/GSL/issues/396>.
While I like this from a compile-time safety perspective, I thought not_null
was meant to be more of a transparent wrapper. I'm not sure if modifying
the API of the wrapped type is within the scope of its design.

But regardless, I don't really understand what you're getting at here.
Post by j***@gmail.com
OK, you have some function that takes a pointer. And your function
implicitly requires that this pointer not be NULL; therefore, it isn't
going to check to see if it's NULL or not. And you intend to store this
not-null pointer around for a time and use it later.
Then why do you not simply *store a pointer*? Why do you need `view<T>`
instead of `T*`? Your code already assumes it's not NULL; what do you gain
from using this type instead of `T*`? That you initialize it with `T&`
rather than `T*`? You still have to use `*` and `->` to access the `T`.
Ideally, the function should not take a pointer if the pointer should not
be null, because it is unsafe (for code implementing the function) and
misleading (for the code calling the function). If the API can't be
changed, not_null is a great way to convey meaning and to add run-time
safety. If the API can be changed, the function should take a reference
instead, because then you have compile-time safety.

The problem with references is that they cannot be reassigned, which makes
them unusable in a lot of generic code (e.g. STL containers). As pointed
out, std::reference_wrapper exists for this purpose, but its API isn't
particularly nice to use as a general-purpose reference wrapper. In my
proposal, I intend view<T> and optional_view<T> to work in tandem as
replacements for T& and T* respectively wherever they represent
"references" (in the general sense). I tried to make the case for view<T>
having some semantic advantage over T&, but you've made me reconsider my
argument, since T& almost always means "reference" (in the general sense)
and T const& is almost always just to avoid an expensive copy. On the other
hand, optional_view<T> does convey additional meaning that T* does not,
since the meaning of T* is so horribly overloaded in C++. I think the case
for view is a lot stronger when it is accompanied by optional_view.

Why not store a pointer? Because it allows bugs to creep into your code.
Pointers can be null, and dereferencing a null pointer results in UB, and
UB is bad. Again, you could use not_null to catch any errors at run time,
but why catch an error at run time when it can be caught at compile time?
Sure, the safety of simple programs can be verified by eye, but not all
programs are simple. If "not-null" pointers are pervasive throughout a
complex system, the chance of null pointer bugs could be high.

Really? In that case, they can be stored in containers, but they don't
Post by j***@gmail.com
Post by j***@gmail.com
Post by j***@gmail.com
really behave 100% like references.
int a = 0;
int b = 0;
int& ra = a;
int& rb = b;
ra = rb; // copies referenced value
smart_ref<int> sra = a;
smart_ref<int> srb = b;
sra = srb; // rebinds reference
I thought the proposal suggested using a special "rebind" function so
that smart references behave exactly like regular references?
And if you want your smart references to work that way, simply declare the
assignment operator `= delete`. But the operator-dot proposal doesn't exist
to tell you what to do with your types; it tells you what you *can do*
with them.
The proposal for operator-dot defines a mechanism, not a policy on how you
build smart references. I imagine that many of them will `=delete` the
operator; maybe all of them. But that is not a question for the
`operator-dot` proposal; it's a question for proposals for actual smart
reference types.
I appreciate the proposal doesn't specify the design of such types. Still,
I'm wondering what possible designs it would enable. If I understand
correctly, you could design two categories of "smart reference":

1. operator=(ref<T> const&) modifies the wrapper
2. operator=(ref<T> const&) modifies the wrapped object

The first option gives consistent behaviour when modifying the wrapper
(copy construct and copy assign do the same thing). However, it makes for
inconsistent behaviour when modifying the wrapper object. For example,
given smart references a and b:


a.foo = y.foo; // modifies wrapped object
a = b; // modifies wrapper

The second option gives consistent behaviour when modifying the wrapped
object, but has inconsistent behaviour when modifying the wrapper. This is
the behaviour of regular references that precludes them from being stored
in containers:

ref<bar> a = b; // modifies (constructs) wrapper
a = b; // modifies wrapped object

I'm sure the operator dot proposal will enable all sorts of great things
via run-time function overriding, but the quest for the perfect "smart
reference" design seems just out of reach. It seems this is because the
design of reference types is fundamentally different from the design of
value types in C++. Thus, view and optional_view do not use operator dot
overloading, because this seems to be the only way to get consistent
behaviour when modifying both the wrapper and when modifying the wrapped
object. You just have to use operator* and operator-> when working with the
underlying object.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/68939a80-a8bc-424d-b01d-08435a5d5d7b%40isocpp.org.
D. B.
2016-10-13 08:34:34 UTC
Permalink
Post by j***@gmail.com
I'm sure the operator dot proposal will enable all sorts of great things
via run-time function overriding
But it's all about *compile-time* overloading.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CACGiwhEDQR_RUx676GTk-ySB24qXTo05OvN4cp_NGhECML%2B_ww%40mail.gmail.com.
j***@gmail.com
2016-10-13 09:12:59 UTC
Permalink
Post by D. B.
Post by j***@gmail.com
I'm sure the operator dot proposal will enable all sorts of great things
via run-time function overriding
But it's all about *compile-time* overloading.
Suppose you could overload functions in the wrapped type as well, assuming
that is how overload resolution works with operator dot (I don't know the
details).
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/3f3ee0d8-6868-4595-b727-39a3a4e3b583%40isocpp.org.
Victor Dyachenko
2016-10-13 08:50:44 UTC
Permalink
Post by j***@gmail.com
Ideally, the function should not take a pointer if the pointer should not
be null, because it is unsafe (for code implementing the function) and
misleading (for the code calling the function). If the API can't be
changed, not_null is a great way to convey meaning and to add run-time
safety. If the API can be changed, the function should take a reference
instead, because then you have compile-time safety.
Sadly, this rule seems non-obvious for people who add any_cast(any * ) and get_if(variant
* ) to the Standard right now...
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/ae1e672d-0e1d-4129-8b14-ffef65224ce9%40isocpp.org.
Nicol Bolas
2016-10-13 16:52:53 UTC
Permalink
Post by j***@gmail.com
Post by j***@gmail.com
That's my point: it's the user's responsibility to check that the pointer
Post by j***@gmail.com
isn't null before dereferencing it. Sure, not_null turns potential UB
into an exception, but at a performance cost (a run time check, and
possible overhead relating to exceptions). By using not_null in our
interface, we impose a performance cost (however small) on the user whether
or not it's even possible for them to pass a null pointer. Using view
(or a plain reference) allows our interface to have zero run time overhead;
we just have to trust the user not to dereference any null pointers they
may have hanging around. But ultimately, even if we use not_null, the
user can still dereference null pointers all day long. If we wanted to
eliminate this possibility, we would be better off encouraging the use of
an alternative to pointers which had no null state -- perhaps something
like view :)
I admit that `not_null<T*>` would be better if it could be constructed
from a `T&`, which would also cause it to not bother checking if it is a
null reference. And I have made such a suggestion
<https://github.com/Microsoft/GSL/issues/396>.
While I like this from a compile-time safety perspective, I thought
not_null was meant to be more of a transparent wrapper. I'm not sure if
modifying the API of the wrapped type is within the scope of its design.
... How does what I suggested modify the API of `T` itself?

But regardless, I don't really understand what you're getting at here.
Post by j***@gmail.com
Post by j***@gmail.com
OK, you have some function that takes a pointer. And your function
implicitly requires that this pointer not be NULL; therefore, it isn't
going to check to see if it's NULL or not. And you intend to store this
not-null pointer around for a time and use it later.
Then why do you not simply *store a pointer*? Why do you need `view<T>`
instead of `T*`? Your code already assumes it's not NULL; what do you gain
from using this type instead of `T*`? That you initialize it with `T&`
rather than `T*`? You still have to use `*` and `->` to access the `T`.
Ideally, the function should not take a pointer if the pointer should not
be null, because it is unsafe (for code implementing the function) and
misleading (for the code calling the function). If the API can't be changed,
not_null is a great way to convey meaning and to add run-time safety.
In what way is adding `not_null` not a change to an API? At the very least,
it adds a user-defined conversion step, from `T*` to `not_null<T*>`. And
that can break code.

For example, many string classes have implicit conversions to `const char
*`. if you have a function that takes a `const char*`, you can pass one of
those string types to it. However, if that API changes to `not_null<const
char *>`, then you can't. That requires two user-defined conversion steps,
and C++ overload resolution doesn't let you do that.

And thus making such a change in the API broke your code. So turning `T*`
into `not_null<T*>` is not a safe change.
Post by j***@gmail.com
If the API can be changed, the function should take a reference instead,
because then you have compile-time safety.
No, you do not have "compile-time safety". What you *have* is
language-level assurance that, if the user somehow managed to pass a null
reference, then the user has *already* caused UB.

But the program as a whole has not been made any safer, either at compile
time or runtime. It simply makes the code more expressive of your intent
(since null references are UB). But so too does `not_null<T*>`.

If a user did this:

foo_not_null(get_a_pointer());

Where `foo_not_null` requires a non-NULL pointer, and `get_a_pointer` could
return NULL. This is runtime-safe. However, doing the following doesn't
become compile-time safe:

foo_view(*get_a_pointer());

Where `foo_view` takes a `view<T>`. No errors are being caught here, at
runtime or compile time. The user *must* check whether the pointer is NULL,
and the user failed to do so.

This program has less safety than the `not_null` version.

At the very least, `view<T>` should have a constructor that takes a `T*`
which throws if the pointer is NULL. Of course, if you did so, that would
make `view<T>` equivalent to my suggested fixed version of `not_null<T*>`.
So where is the advantage for `view<T>`?

The problem with references is that they cannot be reassigned, which makes
Post by j***@gmail.com
them unusable in a lot of generic code (e.g. STL containers). As pointed
out, std::reference_wrapper exists for this purpose, but its API isn't
particularly nice to use as a general-purpose reference wrapper. In my
proposal, I intend view<T> and optional_view<T> to work in tandem as
replacements for T& and T* respectively wherever they represent
"references" (in the general sense). I tried to make the case for view<T>
having some semantic advantage over T&, but you've made me reconsider my
argument, since T& almost always means "reference" (in the general sense)
and T const& is almost always just to avoid an expensive copy. On the
other hand, optional_view<T> does convey additional meaning that T* does
not, since the meaning of T* is so horribly overloaded in C++.
In general, yes. But "in general" is talking about the reams of legacy code
that exists out there. That legacy code isn't going to switch from `T*` to
`optional_view<T>` no matter what.

The C++ core guidelines gives us a reasonably narrow field of usage of
naked pointers: they are nullable, non-owning references to a single `T`.
Exactly like your `optional_view<T>`.

So a user following good coding guidelines will use `T*` only for such
cases.

Though I suppose there is merit to the idea that if you're modernizing a
codebase, you need to distinguish between not-yet-modernized functions
where `T*` means "anything goes", and APIs that have been modernized where
`T*` means "nullable, non-owning references to a single `T`".

I think the case for view is a lot stronger when it is accompanied by
Post by j***@gmail.com
optional_view.
Why not store a pointer? Because it allows bugs to creep into your code.
Pointers can be null, and dereferencing a null pointer results in UB, and
UB is bad. Again, you could use not_null to catch any errors at run time,
but why catch an error at run time when it can be caught at compile time?
Sure, the safety of simple programs can be verified by eye, but not all
programs are simple. If "not-null" pointers are pervasive throughout a
complex system, the chance of null pointer bugs could be high.
Really? In that case, they can be stored in containers, but they don't
Post by j***@gmail.com
Post by j***@gmail.com
Post by j***@gmail.com
really behave 100% like references.
int a = 0;
int b = 0;
int& ra = a;
int& rb = b;
ra = rb; // copies referenced value
smart_ref<int> sra = a;
smart_ref<int> srb = b;
sra = srb; // rebinds reference
I thought the proposal suggested using a special "rebind" function so
that smart references behave exactly like regular references?
And if you want your smart references to work that way, simply declare
the assignment operator `= delete`. But the operator-dot proposal doesn't
exist to tell you what to do with your types; it tells you what you *can
do* with them.
The proposal for operator-dot defines a mechanism, not a policy on how
you build smart references. I imagine that many of them will `=delete` the
operator; maybe all of them. But that is not a question for the
`operator-dot` proposal; it's a question for proposals for actual smart
reference types.
I appreciate the proposal doesn't specify the design of such types. Still,
I'm wondering what possible designs it would enable. If I understand
1. operator=(ref<T> const&) modifies the wrapper
2. operator=(ref<T> const&) modifies the wrapped object
The first option gives consistent behaviour when modifying the wrapper
(copy construct and copy assign do the same thing). However, it makes for
inconsistent behaviour when modifying the wrapper object. For example,
a.foo = y.foo; // modifies wrapped object
a = b; // modifies wrapper
The second option gives consistent behaviour when modifying the wrapped
object, but has inconsistent behaviour when modifying the wrapper. This is
the behaviour of regular references that precludes them from being stored
ref<bar> a = b; // modifies (constructs) wrapper
Post by j***@gmail.com
a = b; // modifies wrapped object
I'm sure the operator dot proposal will enable all sorts of great things
Post by j***@gmail.com
via run-time function overriding,
There's nothing "runtime" about operator-dot. Exactly what function gets
called when dealing with operator-dot types is well-defined at compile time.

but the quest for the perfect "smart reference" design seems just out of
Post by j***@gmail.com
reach.
It is "out of reach" only because your definition of "perfect" is
inherently contradictory. You define "perfection" as emulating C++ language
references exactly, while simultaneously allowing by-reference copying via
operator=, which C++ language references do not do.

Emulating language references is a binary proposition. Either that's
something you want, or its something you don't.

It seems this is because the design of reference types is fundamentally
Post by j***@gmail.com
different from the design of value types in C++. Thus, view and
optional_view do not use operator dot overloading, because this seems to
be the only way to get consistent behaviour when modifying both the wrapper
and when modifying the wrapped object.
And yet, that's not true at all. Your option 1 above seems perfectly
consistent. Just like `a->foo == b->foo`. It only looks odd because you
expect `.` to mean "access the wrapper" instead of "possibly access the
wrapped object".
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/6990498c-47d3-4d1c-85c8-25f68a7fda86%40isocpp.org.
j***@gmail.com
2016-10-14 13:36:56 UTC
Permalink
Post by Nicol Bolas
Post by j***@gmail.com
Post by j***@gmail.com
That's my point: it's the user's responsibility to check that the
pointer isn't null before dereferencing it. Sure, not_null turns
potential UB into an exception, but at a performance cost (a run time
check, and possible overhead relating to exceptions). By using not_null
in our interface, we impose a performance cost (however small) on the user
whether or not it's even possible for them to pass a null pointer. Using
view (or a plain reference) allows our interface to have zero run time
overhead; we just have to trust the user not to dereference any null
pointers they may have hanging around. But ultimately, even if we use
not_null, the user can still dereference null pointers all day long.
If we wanted to eliminate this possibility, we would be better off
encouraging the use of an alternative to pointers which had no null state
-- perhaps something like view :)
I admit that `not_null<T*>` would be better if it could be constructed
from a `T&`, which would also cause it to not bother checking if it is a
null reference. And I have made such a suggestion
<https://github.com/Microsoft/GSL/issues/396>.
While I like this from a compile-time safety perspective, I thought
not_null was meant to be more of a transparent wrapper. I'm not sure if
modifying the API of the wrapped type is within the scope of its design.
... How does what I suggested modify the API of `T` itself?
But regardless, I don't really understand what you're getting at here.
Post by j***@gmail.com
Post by j***@gmail.com
OK, you have some function that takes a pointer. And your function
implicitly requires that this pointer not be NULL; therefore, it isn't
going to check to see if it's NULL or not. And you intend to store this
not-null pointer around for a time and use it later.
Then why do you not simply *store a pointer*? Why do you need `view<T>`
instead of `T*`? Your code already assumes it's not NULL; what do you gain
from using this type instead of `T*`? That you initialize it with `T&`
rather than `T*`? You still have to use `*` and `->` to access the `T`.
Ideally, the function should not take a pointer if the pointer should not
be null, because it is unsafe (for code implementing the function) and
misleading (for the code calling the function). If the API can't be changed,
not_null is a great way to convey meaning and to add run-time safety.
In what way is adding `not_null` not a change to an API? At the very
least, it adds a user-defined conversion step, from `T*` to `not_null<T*>`.
And that can break code.
For example, many string classes have implicit conversions to `const char
*`. if you have a function that takes a `const char*`, you can pass one of
those string types to it. However, if that API changes to `not_null<const
char *>`, then you can't. That requires two user-defined conversion steps,
and C++ overload resolution doesn't let you do that.
And thus making such a change in the API broke your code. So turning `T*`
into `not_null<T*>` is not a safe change.
I didn't mean to be taken so literally. A wrapper modifies the API of the
wrapped object insofar as it presents an alternative API to the user.
Anyway, I'm not in charge of the design of not_null; this was just an
observation based on my understanding.
Post by Nicol Bolas
If the API can be changed, the function should take a reference instead,
Post by j***@gmail.com
because then you have compile-time safety.
No, you do not have "compile-time safety". What you *have* is
language-level assurance that, if the user somehow managed to pass a null
reference, then the user has *already* caused UB.
But the program as a whole has not been made any safer, either at compile
time or runtime. It simply makes the code more expressive of your intent
(since null references are UB). But so too does `not_null<T*>`.
foo_not_null(get_a_pointer());
Where `foo_not_null` requires a non-NULL pointer, and `get_a_pointer`
could return NULL. This is runtime-safe. However, doing the following
foo_view(*get_a_pointer());
Where `foo_view` takes a `view<T>`. No errors are being caught here, at
runtime or compile time. The user *must* check whether the pointer is
NULL, and the user failed to do so.
This program has less safety than the `not_null` version.
At the very least, `view<T>` should have a constructor that takes a `T*`
which throws if the pointer is NULL. Of course, if you did so, that would
make `view<T>` equivalent to my suggested fixed version of `not_null<T*>`.
So where is the advantage for `view<T>`?
Okay, I understand your point. I guess my main problem is with potential
run-time cost where it isn't necessary.

However, I have just realized that the GSL allows the behaviour of contract
violations to be configured (it defaults to calling std::terminate). I
assume this is because we currently lack the static analysis tools that the
GSL is meant to assist. I am now assuming that the run-time check is
intended to be removed in release code (and if not_null were ever
standardized), in favour of static detection of unchecked pointer
dereferencing and conversion to not_null. This is supported by the
description in F.23
<https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f23-use-a-not_nullt-to-indicate-that-null-is-not-a-valid-value>
of the C++ Core Guidelines.

If this were the case, not_null would not in fact be guaranteed to be "not
null" at run-time; the static analyzer would produce a warning of unchecked
conversion to not_null (though this check isn't specified in the C++ Core
Guidelines for some reason) and UB would arise potentially far from the
warning site, wherever the not_null wrapper were eventually dereferenced.
On the other hand, view *would* be guaranteed be "not null" at run-time;
the static analyzer would produce a warning of an unchecked dereference of
a raw pointer *at* the point at which UB arises. Another minor advantage is
that you don't need that extra check that I mentioned.

F.23 mentions that run-time checks can be performed in debug builds, but
most debuggers will catch a null pointer dereference, so I'm not sure how
useful this is.
Post by Nicol Bolas
The problem with references is that they cannot be reassigned, which makes
Post by j***@gmail.com
them unusable in a lot of generic code (e.g. STL containers). As pointed
out, std::reference_wrapper exists for this purpose, but its API isn't
particularly nice to use as a general-purpose reference wrapper. In my
proposal, I intend view<T> and optional_view<T> to work in tandem as
replacements for T& and T* respectively wherever they represent
"references" (in the general sense). I tried to make the case for view<T>
having some semantic advantage over T&, but you've made me reconsider my
argument, since T& almost always means "reference" (in the general
sense) and T const& is almost always just to avoid an expensive copy. On
the other hand, optional_view<T> does convey additional meaning that T*
does not, since the meaning of T* is so horribly overloaded in C++.
In general, yes. But "in general" is talking about the reams of legacy
code that exists out there. That legacy code isn't going to switch from
`T*` to `optional_view<T>` no matter what.
I'm not entirely sure what you are responding to here, but I didn't intend
view or optional_view to be for legacy code in particular (unlike not_null,
which does appear to be geared towards improving the safety of legacy
code). They are intended to compliment existing standard library types with
a higher-level abstraction of the non-owning "reference" concept.
Post by Nicol Bolas
The C++ core guidelines gives us a reasonably narrow field of usage of
naked pointers: they are nullable, non-owning references to a single `T`.
Exactly like your `optional_view<T>`.
So a user following good coding guidelines will use `T*` only for such
cases.
Though I suppose there is merit to the idea that if you're modernizing a
codebase, you need to distinguish between not-yet-modernized functions
where `T*` means "anything goes", and APIs that have been modernized where
`T*` means "nullable, non-owning references to a single `T`".
Not just modernizing an existing code base, but also writing new modern
code. Other features of view and optional_view are also nice, such as being
able to copy views, implicit conversion of view<T> to T& *and* T*,implicit
conversion from T* *and* T& (and view<T>) to optional_view<T>, a "safe
dereferencing" operation in optional_view::value, compatibility of view
with std::propagate_const, and the fact that both view and optional_view
have very similar APIs.

I think the case for view is a lot stronger when it is accompanied by
Post by Nicol Bolas
Post by j***@gmail.com
optional_view.
Why not store a pointer? Because it allows bugs to creep into your code.
Pointers can be null, and dereferencing a null pointer results in UB, and
UB is bad. Again, you could use not_null to catch any errors at run
time, but why catch an error at run time when it can be caught at compile
time? Sure, the safety of simple programs can be verified by eye, but not
all programs are simple. If "not-null" pointers are pervasive throughout a
complex system, the chance of null pointer bugs could be high.
Really? In that case, they can be stored in containers, but they don't
Post by j***@gmail.com
Post by j***@gmail.com
really behave 100% like references.
int a = 0;
int b = 0;
int& ra = a;
int& rb = b;
ra = rb; // copies referenced value
smart_ref<int> sra = a;
smart_ref<int> srb = b;
sra = srb; // rebinds reference
I thought the proposal suggested using a special "rebind" function so
that smart references behave exactly like regular references?
And if you want your smart references to work that way, simply declare
the assignment operator `= delete`. But the operator-dot proposal doesn't
exist to tell you what to do with your types; it tells you what you *can
do* with them.
The proposal for operator-dot defines a mechanism, not a policy on how
you build smart references. I imagine that many of them will `=delete` the
operator; maybe all of them. But that is not a question for the
`operator-dot` proposal; it's a question for proposals for actual smart
reference types.
I appreciate the proposal doesn't specify the design of such types.
Still, I'm wondering what possible designs it would enable. If I understand
1. operator=(ref<T> const&) modifies the wrapper
2. operator=(ref<T> const&) modifies the wrapped object
The first option gives consistent behaviour when modifying the wrapper
(copy construct and copy assign do the same thing). However, it makes for
inconsistent behaviour when modifying the wrapper object. For example,
a.foo = y.foo; // modifies wrapped object
a = b; // modifies wrapper
The second option gives consistent behaviour when modifying the wrapped
object, but has inconsistent behaviour when modifying the wrapper. This is
the behaviour of regular references that precludes them from being stored
ref<bar> a = b; // modifies (constructs) wrapper
Post by j***@gmail.com
a = b; // modifies wrapped object
I'm sure the operator dot proposal will enable all sorts of great things
Post by j***@gmail.com
via run-time function overriding,
There's nothing "runtime" about operator-dot. Exactly what function gets
called when dealing with operator-dot types is well-defined at compile time.
Sorry, that was a typo. I meant compile-time.
Post by Nicol Bolas
but the quest for the perfect "smart reference" design seems just out of
Post by j***@gmail.com
reach.
It is "out of reach" only because your definition of "perfect" is
inherently contradictory. You define "perfection" as emulating C++ language
references exactly, while simultaneously allowing by-reference copying via
operator=, which C++ language references do not do.
Emulating language references is a binary proposition. Either that's
something you want, or its something you don't.
I'm conflating C++ references and references in a general sense again. It
is possible to perfectly emulate C++ references (that would be my option
2). What I want (consistent behaviour for copying both wrapper and wrapped
objects) is out of reach when using operator dot overloading, so I don't
use it.
Post by Nicol Bolas
It seems this is because the design of reference types is fundamentally
Post by j***@gmail.com
different from the design of value types in C++. Thus, view and
optional_view do not use operator dot overloading, because this seems to
be the only way to get consistent behaviour when modifying both the wrapper
and when modifying the wrapped object.
And yet, that's not true at all. Your option 1 above seems perfectly
consistent. Just like `a->foo == b->foo`. It only looks odd because you
expect `.` to mean "access the wrapper" instead of "possibly access the
wrapped object".
When I say it isn't consistent, I mean that if a.foo = b.foo modifies the
wrapped object, then I expect a = b to modify the wrapped object as well.
The problem is that there is only one operator=, and two functions I want
it to perform. This is why I must, unfortunately, rely on operator* and
operator-> when referring to the wrapped object.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/3ab79d96-c0a3-4960-a2ef-e88850d1b1c7%40isocpp.org.
Nicol Bolas
2016-10-14 15:23:15 UTC
Permalink
Post by j***@gmail.com
Post by Nicol Bolas
No, you do not have "compile-time safety". What you *have* is
language-level assurance that, if the user somehow managed to pass a null
reference, then the user has *already* caused UB.
But the program as a whole has not been made any safer, either at compile
time or runtime. It simply makes the code more expressive of your intent
(since null references are UB). But so too does `not_null<T*>`.
foo_not_null(get_a_pointer());
Where `foo_not_null` requires a non-NULL pointer, and `get_a_pointer`
could return NULL. This is runtime-safe. However, doing the following
foo_view(*get_a_pointer());
Where `foo_view` takes a `view<T>`. No errors are being caught here, at
runtime or compile time. The user *must* check whether the pointer is
NULL, and the user failed to do so.
This program has less safety than the `not_null` version.
At the very least, `view<T>` should have a constructor that takes a `T*`
which throws if the pointer is NULL. Of course, if you did so, that would
make `view<T>` equivalent to my suggested fixed version of `not_null<T*>`.
So where is the advantage for `view<T>`?
Okay, I understand your point. I guess my main problem is with potential
run-time cost where it isn't necessary.
Which, if my change for `not_null` goes through, can be easily mitigated.
The cost only happens when the pointer is introduced to `not_null`. If you
pass a reference, there's no check.
Post by j***@gmail.com
However, I have just realized that the GSL allows the behaviour of
contract violations to be configured (it defaults to calling
std::terminate). I assume this is because we currently lack the static
analysis tools that the GSL is meant to assist. I am now assuming that the
run-time check is intended to be removed in release code (and if not_null
were ever standardized), in favour of static detection of unchecked pointer
dereferencing and conversion to not_null. This is supported by the
description in F.23
<https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f23-use-a-not_nullt-to-indicate-that-null-is-not-a-valid-value>
of the C++ Core Guidelines.
If this were the case, not_null would not in fact be guaranteed to be
"not null" at run-time; the static analyzer would produce a warning of
unchecked conversion to not_null (though this check isn't specified in
the C++ Core Guidelines for some reason) and UB would arise potentially far
from the warning site, wherever the not_null wrapper were eventually
dereferenced. On the other hand, view *would* be guaranteed be "not null"
at run-time; the static analyzer would produce a warning of an unchecked
dereference of a raw pointer *at* the point at which UB arises. Another
minor advantage is that you don't need that extra check that I mentioned.
F.23 mentions that run-time checks can be performed in debug builds, but
most debuggers will catch a null pointer dereference, so I'm not sure how
useful this is.
It's very useful. A debugger can detect a NULL pointer dereference, but
only at the cite of use, not the place where the NULL pointer *came from*.
If the code that stored the pointer had used `not_null`, then they could
get an error at the source of the pointer. Or at least, at the edges of the
system that expected it to not be NULL, rather than wherever it first got
used.

The problem with references is that they cannot be reassigned, which makes
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
them unusable in a lot of generic code (e.g. STL containers). As pointed
out, std::reference_wrapper exists for this purpose, but its API isn't
particularly nice to use as a general-purpose reference wrapper. In my
proposal, I intend view<T> and optional_view<T> to work in tandem as
replacements for T& and T* respectively wherever they represent
"references" (in the general sense). I tried to make the case for
view<T> having some semantic advantage over T&, but you've made me
reconsider my argument, since T& almost always means "reference" (in
the general sense) and T const& is almost always just to avoid an
expensive copy. On the other hand, optional_view<T> does convey
additional meaning that T* does not, since the meaning of T* is so
horribly overloaded in C++.
In general, yes. But "in general" is talking about the reams of legacy
code that exists out there. That legacy code isn't going to switch from
`T*` to `optional_view<T>` no matter what.
I'm not entirely sure what you are responding to here, but I didn't intend
view or optional_view to be for legacy code in particular (unlike
not_null, which does appear to be geared towards improving the safety of
legacy code). They are intended to compliment existing standard library
types with a higher-level abstraction of the non-owning "reference" concept.
What I'm getting at is that, in code written for modern C++, it is
reasonable to assume that `T*` has a specific meaning: nullable, non-owning
reference to a single object. So if you're writing modern C++, you don't
need `optional_view<T>` to say what the much shorter `T*` already says.

So `optional_view<T>` is only advantageous when working with a codebase
where `T*` does not consistently have a specific meaning. Hopefully, we're
not writing more of that kind of code...

but the quest for the perfect "smart reference" design seems just out of
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
reach.
It is "out of reach" only because your definition of "perfect" is
inherently contradictory. You define "perfection" as emulating C++ language
references exactly, while simultaneously allowing by-reference copying via
operator=, which C++ language references do not do.
Emulating language references is a binary proposition. Either that's
something you want, or its something you don't.
I'm conflating C++ references and references in a general sense again. It
is possible to perfectly emulate C++ references (that would be my option
2). What I want (consistent behaviour for copying both wrapper and wrapped
objects) is out of reach when using operator dot overloading, so I don't
use it.
Post by Nicol Bolas
It seems this is because the design of reference types is fundamentally
Post by j***@gmail.com
different from the design of value types in C++. Thus, view and
optional_view do not use operator dot overloading, because this seems
to be the only way to get consistent behaviour when modifying both the
wrapper and when modifying the wrapped object.
And yet, that's not true at all. Your option 1 above seems perfectly
consistent. Just like `a->foo == b->foo`. It only looks odd because you
expect `.` to mean "access the wrapper" instead of "possibly access the
wrapped object".
When I say it isn't consistent, I mean that if a.foo = b.foo modifies the
wrapped object, then I expect a = b to modify the wrapped object as well.
The problem is that there is only one operator=, and two functions I want
it to perform. This is why I must, unfortunately, rely on operator* and
operator-> when referring to the wrapped object.
So... why is it consistent for `a->foo = b->foo` to have different behavior
from `a = b`, yet `a.foo = b.foo` should have the same behavior?

The answer is simple: because you expect `->` to mean "access wrapped
object", while you expect `.` to mean "access handle". You live in a world
sans-operator-dot, so you don't expect `a.foo` to potentially access the
wrapped object. Consistency is based on expectations.

Operator-dot represents a fundamental shift in our expectations.

And that's probably the scariest part of it, and the prime reason why I
don't think it should exist.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/6ca9b6c7-cd53-463e-b93f-7036111eef70%40isocpp.org.
j***@gmail.com
2016-10-14 16:14:30 UTC
Permalink
Post by Nicol Bolas
Post by j***@gmail.com
Post by Nicol Bolas
No, you do not have "compile-time safety". What you *have* is
language-level assurance that, if the user somehow managed to pass a null
reference, then the user has *already* caused UB.
But the program as a whole has not been made any safer, either at
compile time or runtime. It simply makes the code more expressive of your
intent (since null references are UB). But so too does `not_null<T*>`.
foo_not_null(get_a_pointer());
Where `foo_not_null` requires a non-NULL pointer, and `get_a_pointer`
could return NULL. This is runtime-safe. However, doing the following
foo_view(*get_a_pointer());
Where `foo_view` takes a `view<T>`. No errors are being caught here, at
runtime or compile time. The user *must* check whether the pointer is
NULL, and the user failed to do so.
This program has less safety than the `not_null` version.
At the very least, `view<T>` should have a constructor that takes a `T*`
which throws if the pointer is NULL. Of course, if you did so, that would
make `view<T>` equivalent to my suggested fixed version of `not_null<T*>`.
So where is the advantage for `view<T>`?
Okay, I understand your point. I guess my main problem is with potential
run-time cost where it isn't necessary.
Which, if my change for `not_null` goes through, can be easily mitigated.
The cost only happens when the pointer is introduced to `not_null`. If you
pass a reference, there's no check.
Yup. I'll be watching your issue report. I'm interested to find out more
about the intended design of not_null.
Post by Nicol Bolas
However, I have just realized that the GSL allows the behaviour of
Post by j***@gmail.com
contract violations to be configured (it defaults to calling
std::terminate). I assume this is because we currently lack the static
analysis tools that the GSL is meant to assist. I am now assuming that the
run-time check is intended to be removed in release code (and if not_null
were ever standardized), in favour of static detection of unchecked pointer
dereferencing and conversion to not_null. This is supported by the
description in F.23
<https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f23-use-a-not_nullt-to-indicate-that-null-is-not-a-valid-value>
of the C++ Core Guidelines.
If this were the case, not_null would not in fact be guaranteed to be
"not null" at run-time; the static analyzer would produce a warning of
unchecked conversion to not_null (though this check isn't specified in
the C++ Core Guidelines for some reason) and UB would arise potentially far
from the warning site, wherever the not_null wrapper were eventually
dereferenced. On the other hand, view *would* be guaranteed be "not
null" at run-time; the static analyzer would produce a warning of an
unchecked dereference of a raw pointer *at* the point at which UB
arises. Another minor advantage is that you don't need that extra check
that I mentioned.
F.23 mentions that run-time checks can be performed in debug builds, but
most debuggers will catch a null pointer dereference, so I'm not sure how
useful this is.
It's very useful. A debugger can detect a NULL pointer dereference, but
only at the cite of use, not the place where the NULL pointer *came from*.
If the code that stored the pointer had used `not_null`, then they could
get an error at the source of the pointer. Or at least, at the edges of the
system that expected it to not be NULL, rather than wherever it first got
used.
Sorry, you are right. It is important to the design of not_null. I was
getting not_null<T*> and T& mixed up in my mind. Null pointer dereference
will only happen at the source when using T& instead of not_null<T*>.
Post by Nicol Bolas
The problem with references is that they cannot be reassigned, which makes
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
them unusable in a lot of generic code (e.g. STL containers). As pointed
out, std::reference_wrapper exists for this purpose, but its API isn't
particularly nice to use as a general-purpose reference wrapper. In my
proposal, I intend view<T> and optional_view<T> to work in tandem as
replacements for T& and T* respectively wherever they represent
"references" (in the general sense). I tried to make the case for
view<T> having some semantic advantage over T&, but you've made me
reconsider my argument, since T& almost always means "reference" (in
the general sense) and T const& is almost always just to avoid an
expensive copy. On the other hand, optional_view<T> does convey
additional meaning that T* does not, since the meaning of T* is so
horribly overloaded in C++.
In general, yes. But "in general" is talking about the reams of legacy
code that exists out there. That legacy code isn't going to switch from
`T*` to `optional_view<T>` no matter what.
I'm not entirely sure what you are responding to here, but I didn't
intend view or optional_view to be for legacy code in particular (unlike
not_null, which does appear to be geared towards improving the safety of
legacy code). They are intended to compliment existing standard library
types with a higher-level abstraction of the non-owning "reference" concept.
What I'm getting at is that, in code written for modern C++, it is
reasonable to assume that `T*` has a specific meaning: nullable, non-owning
reference to a single object. So if you're writing modern C++, you don't
need `optional_view<T>` to say what the much shorter `T*` already says.
So `optional_view<T>` is only advantageous when working with a codebase
where `T*` does not consistently have a specific meaning. Hopefully, we're
not writing more of that kind of code...
This is a fair argument, and a reasonable conclusion if you don't find any
of the other features of view and optional_view compelling.
Post by Nicol Bolas
but the quest for the perfect "smart reference" design seems just out of
Post by j***@gmail.com
Post by Nicol Bolas
Post by j***@gmail.com
reach.
It is "out of reach" only because your definition of "perfect" is
inherently contradictory. You define "perfection" as emulating C++ language
references exactly, while simultaneously allowing by-reference copying via
operator=, which C++ language references do not do.
Emulating language references is a binary proposition. Either that's
something you want, or its something you don't.
I'm conflating C++ references and references in a general sense again. It
is possible to perfectly emulate C++ references (that would be my option
2). What I want (consistent behaviour for copying both wrapper and wrapped
objects) is out of reach when using operator dot overloading, so I don't
use it.
Post by Nicol Bolas
It seems this is because the design of reference types is fundamentally
Post by j***@gmail.com
different from the design of value types in C++. Thus, view and
optional_view do not use operator dot overloading, because this seems
to be the only way to get consistent behaviour when modifying both the
wrapper and when modifying the wrapped object.
And yet, that's not true at all. Your option 1 above seems perfectly
consistent. Just like `a->foo == b->foo`. It only looks odd because you
expect `.` to mean "access the wrapper" instead of "possibly access the
wrapped object".
When I say it isn't consistent, I mean that if a.foo = b.foo modifies
the wrapped object, then I expect a = b to modify the wrapped object as
well. The problem is that there is only one operator=, and two functions
I want it to perform. This is why I must, unfortunately, rely on
operator* and operator-> when referring to the wrapped object.
So... why is it consistent for `a->foo = b->foo` to have different
behavior from `a = b`, yet `a.foo = b.foo` should have the same behavior?
Maybe it will be clearer if I write a->foo = b->foo in a different way.
This is the "consistent" behaviour I want:

(*a) = (*b); // modifies wrapped object
(*a).foo = (*b).foo; // modifies wrapped object

a.foo = b.foo; // modifies wrapper
a = b; // modifies wrapper

The answer is simple: because you expect `->` to mean "access wrapped
Post by Nicol Bolas
object", while you expect `.` to mean "access handle". You live in a world
sans-operator-dot, so you don't expect `a.foo` to potentially access the
wrapped object. Consistency is based on expectations.
So you understand what I'm saying. You just wouldn't call it "consistency".
Perhaps "symmetry" is a better term. Operator dot overloading breaks the
symmetry of C++ object syntax. Of course, C++ references already have this
asymmetry, but I wonder if this too was a mistake: perhaps references
should have been designed to be de*referenced* like pointers. Of course,
this opens up a whole new incredibly complex can of worms, but it's an
interesting thought. Perhaps this is the idea I am trying to model with view
and optional_view.
Post by Nicol Bolas
Operator-dot represents a fundamental shift in our expectations.
And that's probably the scariest part of it, and the prime reason why I
don't think it should exist.
You don't? I assumed you were all for it. Well, then we agree on something
:)
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/5c4295d5-c02b-4747-bc7b-b5eb707e5fba%40isocpp.org.
m***@gmail.com
2016-10-15 09:37:26 UTC
Permalink
Post by j***@gmail.com
...
Post by Nicol Bolas
Operator-dot represents a fundamental shift in our expectations.
And that's probably the scariest part of it, and the prime reason why I
don't think it should exist.
You don't? I assumed you were all for it. Well, then we agree on something
:)
I am not sure, you all aware of the one of the other proposals. Namely
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0352r0.pdf
<http://www.google.com/url?q=http%3A%2F%2Fwww.open-std.org%2Fjtc1%2Fsc22%2Fwg21%2Fdocs%2Fpapers%2F2016%2Fp0352r0.pdf&sa=D&sntz=1&usg=AFQjCNHYZPJtfbho-i-q7MgRJPt5k81BRw>
It does not override operator.(). It "just" reuses operator T() to "convert
to" the required object.

The trick is, it advertises possible convert-on-dot classes, reusing the
"subclass" semantics and syntax.
This way using the dot to access a different object no longer feels like
magic, because all the possible interfaces of the class are stated
as "subclasses" in the class definition, like they always had!

I personally like this proposal very much because it uses old mechanics to
create new scenarios.
It feels like a natural extension to both.

Consider:

struct A
{
int i = 1;
};

struct B
{
operator A&() { return a; }
A a;
};

int main() {

B b;
auto i = static_cast<A&>(b).i;

std::cout << i;
}

This is possible and valid today.

As well as:


struct A
{
int i = 1;
};

struct B : public A
{

};

int main() {

B b;
auto i = b.i;

std::cout << i;
}


And with the proposal:

struct A
{
int i = 1;
};

struct B : public using A
{
operator A&() { return a; }
A a;
};

int main() {

B b;
auto i = b.i;

std::cout << i;
}

*Does not look that bad at all -* You mix both to get the benefits of both!
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/5a441473-cd1d-48e8-8069-c95f78b9bb68%40isocpp.org.
j***@gmail.com
2016-10-19 07:47:09 UTC
Permalink
Thanks for the info. This actually seems like a very nice proposal. In
fact, it has made me think about how propagate_const could be implemented
more flexibly with such a feature.
Post by m***@gmail.com
...
Post by j***@gmail.com
Post by Nicol Bolas
Operator-dot represents a fundamental shift in our expectations.
And that's probably the scariest part of it, and the prime reason why I
don't think it should exist.
You don't? I assumed you were all for it. Well, then we agree on
something :)
I am not sure, you all aware of the one of the other proposals. Namely
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0352r0.pdf
<http://www.google.com/url?q=http%3A%2F%2Fwww.open-std.org%2Fjtc1%2Fsc22%2Fwg21%2Fdocs%2Fpapers%2F2016%2Fp0352r0.pdf&sa=D&sntz=1&usg=AFQjCNHYZPJtfbho-i-q7MgRJPt5k81BRw>
It does not override operator.(). It "just" reuses operator T() to
"convert to" the required object.
The trick is, it advertises possible convert-on-dot classes, reusing the
"subclass" semantics and syntax.
This way using the dot to access a different object no longer feels like
magic, because all the possible interfaces of the class are stated
as "subclasses" in the class definition, like they always had!
I personally like this proposal very much because it uses old mechanics to
create new scenarios.
It feels like a natural extension to both.
struct A
{
int i = 1;
};
struct B
{
operator A&() { return a; }
A a;
};
int main() {
B b;
auto i = static_cast<A&>(b).i;
std::cout << i;
}
This is possible and valid today.
struct A
{
int i = 1;
};
struct B : public A
{
};
int main() {
B b;
auto i = b.i;
std::cout << i;
}
struct A
{
int i = 1;
};
struct B : public using A
{
operator A&() { return a; }
A a;
};
int main() {
B b;
auto i = b.i;
std::cout << i;
}
*Does not look that bad at all -* You mix both to get the benefits of both!
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/74d6d50b-fe9e-4517-a18b-0316ffb96060%40isocpp.org.
Victor Dyachenko
2016-10-12 07:34:09 UTC
Permalink
What is the difference between std::view and std::reference_wrapper? Just
different syntax here?

std::view<int> v = i;
v = j; // reassignment of the view
*v = 42; // assignment of the referenced object

std::reference_wrapper<int> v = i;
v = std::ref(j); // reassignment of the reference
v.get() = 42; // assignment of the referenced object
//or just
v = 42; // assignment of the referenced object
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/53860757-5e7d-4799-a793-b4b25569c2f2%40isocpp.org.
j***@gmail.com
2016-10-12 07:52:09 UTC
Permalink
Yes, I intended it to have more natural syntax than std::reference_wrapper.
view and optional_view are intended to be used in certain places where
references and pointers are used at the moment, and are designed to be
interoperable. In addition, the way they are designed at the moment, they
are compatible with the proposed std::propagate_const wrapper (implicit
conversion to T& may not work well with this, but like I said, I haven't
figured out all the details).

By the way, I don't think you can do v = 42 with a std::reference_wrapper.
AFAIK, you have to use get.
Post by Victor Dyachenko
What is the difference between std::view and std::reference_wrapper? Just
different syntax here?
std::view<int> v = i;
v = j; // reassignment of the view
*v = 42; // assignment of the referenced object
std::reference_wrapper<int> v = i;
v = std::ref(j); // reassignment of the reference
v.get() = 42; // assignment of the referenced object
//or just
v = 42; // assignment of the referenced object
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/f0424841-8423-4cf5-b01b-34fea528b46e%40isocpp.org.
Victor Dyachenko
2016-10-12 08:05:38 UTC
Permalink
Post by j***@gmail.com
By the way, I don't think you can do v = 42 with a std::reference_wrapper.
AFAIK, you have to use get.
Yes..
r = std::ref(value);
and
r = value;
are equivalent.
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a0554a67-930e-4c5c-8780-e477864669d0%40isocpp.org.
Victor Dyachenko
2016-10-12 08:11:11 UTC
Permalink
So difference is just *v vs v.get()
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/0bba66f6-358c-493e-a47c-b9887ee46674%40isocpp.org.
j***@gmail.com
2016-10-12 08:31:46 UTC
Permalink
Yes, because it is modelled after other wrapper types like std::unique_ptr
and std::optional. std::reference_wrapper is, as far as I know, intended
for use in implementation code. At least, I've never seen it used in an API.
Post by Victor Dyachenko
So difference is just *v vs v.get()
--
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+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/3ed0318a-d385-4a5a-b4d4-0df4fcf525d2%40isocpp.org.
Loading...