Proposal: std::view and std::optional_view (Re: operator dot proposal)

339 views
Skip to first unread message

joseph....@gmail.com

unread,
Oct 10, 2016, 11:10:37 PM10/10/16
to ISO C++ Standard - Future Proposals
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 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 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

D. B.

unread,
Oct 11, 2016, 2:07:32 AM10/11/16
to std-pr...@isocpp.org
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.

joseph....@gmail.com

unread,
Oct 11, 2016, 2:54:44 AM10/11/16
to ISO C++ Standard - Future Proposals


On Tuesday, 11 October 2016 14:07:32 UTC+8, D. B. wrote:
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&)

 

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));


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".

mihailn...@gmail.com

unread,
Oct 11, 2016, 3:24:19 AM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@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!  

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.

joseph....@gmail.com

unread,
Oct 11, 2016, 4:18:43 AM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com

On Tuesday, 11 October 2016 15:24:19 UTC+8, mihailn...@gmail.com wrote:
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.
 

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).

D. B.

unread,
Oct 11, 2016, 4:45:03 AM10/11/16
to std-pr...@isocpp.org
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.

D. B.

unread,
Oct 11, 2016, 4:48:18 AM10/11/16
to std-pr...@isocpp.org
On Tue, Oct 11, 2016 at 7:54 AM, <joseph....@gmail.com> wrote:
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.


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'.
 

joseph....@gmail.com

unread,
Oct 11, 2016, 5:17:47 AM10/11/16
to ISO C++ Standard - Future Proposals
 
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 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.
 
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 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.

D. B.

unread,
Oct 11, 2016, 5:24:35 AM10/11/16
to std-pr...@isocpp.org
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. :)

joseph....@gmail.com

unread,
Oct 11, 2016, 5:46:45 AM10/11/16
to ISO C++ Standard - Future Proposals
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.

mihailn...@gmail.com

unread,
Oct 11, 2016, 7:46:38 AM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@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.  

joseph....@gmail.com

unread,
Oct 11, 2016, 8:03:21 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@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.

Nicol Bolas

unread,
Oct 11, 2016, 8:19:53 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Tuesday, October 11, 2016 at 8:03:21 PM UTC-4, joseph....@gmail.com wrote:
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.

joseph....@gmail.com

unread,
Oct 11, 2016, 9:09:20 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@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
  }

joseph....@gmail.com

unread,
Oct 11, 2016, 9:24:06 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@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 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)
    ...
  }

Nicol Bolas

unread,
Oct 11, 2016, 10:06:12 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Tuesday, October 11, 2016 at 9:24:06 PM UTC-4, joseph....@gmail.com wrote:
On Wednesday, 12 October 2016 09:09:20 UTC+8, joseph....@gmail.com wrote:
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 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)
    ...
  }

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 `->`.

Nicol Bolas

unread,
Oct 11, 2016, 10:08:15 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com

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.

joseph....@gmail.com

unread,
Oct 11, 2016, 11:54:27 PM10/11/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com

  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 `->`.

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 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.

`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.

Nicol Bolas

unread,
Oct 12, 2016, 2:17:19 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com

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.
 
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.
 
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. 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>.

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.

joseph....@gmail.com

unread,
Oct 12, 2016, 3:07:15 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
 
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.

`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.

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.
 
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>.

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.

Victor Dyachenko

unread,
Oct 12, 2016, 3:34:10 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com
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

joseph....@gmail.com

unread,
Oct 12, 2016, 3:52:10 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com
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.

Victor Dyachenko

unread,
Oct 12, 2016, 4:05:38 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com
On Wednesday, October 12, 2016 at 10:52:10 AM UTC+3, joseph....@gmail.com wrote:
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.

Victor Dyachenko

unread,
Oct 12, 2016, 4:11:11 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com
So difference is just *v vs v.get()

joseph....@gmail.com

unread,
Oct 12, 2016, 4:31:46 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com
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.

Nicol Bolas

unread,
Oct 12, 2016, 9:43:24 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Wednesday, October 12, 2016 at 3:07:15 AM UTC-4, joseph....@gmail.com wrote:
On Wednesday, 12 October 2016 14:17:19 UTC+8, Nicol Bolas wrote:
On Tuesday, October 11, 2016 at 11:54:27 PM UTC-4, joseph....@gmail.com wrote:
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.
 
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.

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.
 

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>.

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.

joseph....@gmail.com

unread,
Oct 12, 2016, 11:47:21 AM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com


On Wednesday, 12 October 2016 21:43:24 UTC+8, Nicol Bolas wrote:
On Wednesday, October 12, 2016 at 3:07:15 AM UTC-4, joseph....@gmail.com wrote:
On Wednesday, 12 October 2016 14:17:19 UTC+8, Nicol Bolas wrote:
On Tuesday, October 11, 2016 at 11:54:27 PM UTC-4, joseph....@gmail.com wrote:
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.
 
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.

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.

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 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>.

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?

Nicol Bolas

unread,
Oct 12, 2016, 2:31:06 PM10/12/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Wednesday, October 12, 2016 at 11:47:21 AM UTC-4, joseph....@gmail.com wrote:
On Wednesday, 12 October 2016 21:43:24 UTC+8, Nicol Bolas wrote:
On Wednesday, October 12, 2016 at 3:07:15 AM UTC-4, joseph....@gmail.com wrote:
On Wednesday, 12 October 2016 14:17:19 UTC+8, Nicol Bolas wrote:
On Tuesday, October 11, 2016 at 11:54:27 PM UTC-4, joseph....@gmail.com wrote:
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.
 
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.

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.

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.

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`.


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>.

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.

joseph....@gmail.com

unread,
Oct 13, 2016, 4:27:51 AM10/13/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Thursday, 13 October 2016 02:31:06 UTC+8, Nicol Bolas wrote:
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.

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.

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 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.

D. B.

unread,
Oct 13, 2016, 4:34:36 AM10/13/16
to std-pr...@isocpp.org
On Thu, Oct 13, 2016 at 9:27 AM, <joseph....@gmail.com> wrote:
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.

Victor Dyachenko

unread,
Oct 13, 2016, 4:50:45 AM10/13/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Thursday, October 13, 2016 at 11:27:51 AM UTC+3, joseph....@gmail.com wrote:
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...

joseph....@gmail.com

unread,
Oct 13, 2016, 5:13:00 AM10/13/16
to ISO C++ Standard - Future Proposals

 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).

Nicol Bolas

unread,
Oct 13, 2016, 12:52:53 PM10/13/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Thursday, October 13, 2016 at 4:27:51 AM UTC-4, joseph....@gmail.com wrote:
On Thursday, 13 October 2016 02:31:06 UTC+8, Nicol Bolas wrote:
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.

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.

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.
 
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 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 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 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,

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 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 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".

joseph....@gmail.com

unread,
Oct 14, 2016, 9:36:56 AM10/14/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Friday, 14 October 2016 00:52:53 UTC+8, Nicol Bolas wrote:
On Thursday, October 13, 2016 at 4:27:51 AM UTC-4, joseph....@gmail.com wrote:
On Thursday, 13 October 2016 02:31:06 UTC+8, Nicol Bolas wrote:
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.

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.

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.
 
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>`?

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 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.
 
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++.

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.
 
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.


Sorry, that was a typo. I meant compile-time.
 
but the quest for the perfect "smart reference" design seems just out of 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.
 
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.

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.

Nicol Bolas

unread,
Oct 14, 2016, 11:23:15 AM10/14/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Friday, October 14, 2016 at 9:36:56 AM UTC-4, joseph....@gmail.com wrote:
On Friday, 14 October 2016 00:52:53 UTC+8, Nicol Bolas wrote:
On Thursday, October 13, 2016 at 4:27:51 AM UTC-4, joseph....@gmail.com wrote:
On Thursday, 13 October 2016 02:31:06 UTC+8, Nicol Bolas wrote: 

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>`?

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.
 
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 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 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 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.
 
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.

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.

joseph....@gmail.com

unread,
Oct 14, 2016, 12:14:30 PM10/14/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
On Friday, 14 October 2016 23:23:15 UTC+8, Nicol Bolas wrote:
On Friday, October 14, 2016 at 9:36:56 AM UTC-4, joseph....@gmail.com wrote:
On Friday, 14 October 2016 00:52:53 UTC+8, Nicol Bolas wrote:
On Thursday, October 13, 2016 at 4:27:51 AM UTC-4, joseph....@gmail.com wrote:
On Thursday, 13 October 2016 02:31:06 UTC+8, Nicol Bolas wrote: 

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>`?

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.
 
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 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*>.
 
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++.

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.
 
but the quest for the perfect "smart reference" design seems just out of 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.
 
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.

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 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 dereferenced 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.
 
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 :)

mihailn...@gmail.com

unread,
Oct 15, 2016, 5:37:27 AM10/15/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
...
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
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!

joseph....@gmail.com

unread,
Oct 19, 2016, 3:47:10 AM10/19/16
to ISO C++ Standard - Future Proposals, joseph....@gmail.com, mihailn...@gmail.com
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.
Reply all
Reply to author
Forward
0 new messages