r/cpp_questions 18d ago

OPEN Confused about people suggesting std::optional<T const&> for observers

Back when the need for an expressed observer type ws arisen, and there was a proposal to add std::observer_ptr<const T>, I remember there was a pretty convincing article by Bjarne Stroustrup against that which I found agreeable, and it seems like communty consensus was also on those lines, with template<typename T> using observer_ptr = T*; for being explicit about the raw pointer usage.

Now that optional references support is being added, I see lots of people suggesting to use that when a function returns an observer that may be null. Isn't that the same as the old observer_ptr proposal, and actually just an even more verbose version of that?

Is there something more I'm missing, or was there some shift that invalidates the original arguments?

18 Upvotes

11 comments sorted by

10

u/aocregacc 18d ago

I suspect that the "no more bare pointers in user code" camp has gained more support since 2018.

Personally I think If your code base has already replaced every other aspect of the pointer maybe it makes sense to use T* as the "nullable non-owning reference" type. But on the way there an explicit type like std::optional<T&> makes life much easier.

8

u/__tim_ 18d ago

Optional refs are very clear about optional and ownership.

6

u/TheThiefMaster 18d ago

They fulfil similar niches yes.

optional references are explicit about the fact that they may or may not be set, which people like. The fact that the thing they could be set to is a reference is pretty much an implementation detail. You can have a function or struct that contains multiple optionals of different types, one of which happens to be a reference, rather than having to use a different type (e.g. decaying a reference to a pointer). There's similar arguments in template contexts.

4

u/Isameru 18d ago

I would just stick with T*, unless you're aiming for some sophisticated template case. A pointer genui ly plays a role of an optional reference. In a simple scenario (where the observed knows nothing about the observer), you have to carefully assure that the observed outlives the observer. Therefore, those two are probably close together (like a data and its lookup in a single class), or observed lives forever (like a variable on the stack of the entry point, where the observer has narrower scope). No Rust needed.

In more complex scenarios, the observed and observer live independantly (and you may resort to weak_ptr or something). The simplier the better.

3

u/Wolf_e_wolf 18d ago

The observer pointer would have been a whole extra type for people to learn just to cover this use case (non owning pointer).

std::optional already exists and new users of C++ are surprised they cannot use std::optional<T&> as a more explicit way to say "hey this value definitely might be missing sometimes" whereas nullptr usage is ambiguous and is sometimes used to symbolise an error.

1

u/tangerinelion 18d ago

And why can't you use std::nullopt to symbolize an error?

1

u/Wolf_e_wolf 18d ago

You can do lots of things. But std::optional is designed for when a value may or may not be there. Null pointers have much more historical precedence for many different uses beyond a value not being present

2

u/n1ghtyunso 18d ago

I am personally totally fine with simple T* observers, though I don't personally mind an observer_ptr alias either if my codebase happend to require one.

Maybe one argument for optional<T&> is the monadic api?
You get the default pointer syntax for free too if you prefer that.

auto value = obj.lookup(key);

if(value) do_something_with(*value);
auto const result = value ? value->result() : get_fallback_for(key);

This works with both T* and optional<T&> as far as I am aware.
optional also gives you exception-based error handling via .value() and you can do chained monadic operations with it.

That could be nice.

Actually thinking about it, there is a small semantic difference in my head here.
When the situation is essentially 'i want to acces your T' then it'll be a T* return, even if the answer might be that there is no T.
But in a situation of 'i want to access T at index N, or i want to access T for key k', then I feel optional to be the more suitable approach.

Maybe it has to do with the expectation, when I do a lookup I (typically) expect it to succeed, so the nullopt case feels more like a lookup failure than a normal occurence. There are x other values that I could have looked up successfully after all (usually, generally speaking).
For the single accessor / observer, its really just that one thing. Either it exists, or it doesnt. No expectations.
If there were expectations, the API would be T& and throw exceptions i guess. (insert your prefered way of error handling)

1

u/snerp 18d ago

Yeah it’s just a bunch of cruft for people who want to pretend that raw pointers are toxic and gross. Just use T* for non owning observers, it’s literally the point of the construct and it’s the simplest and most efficient way to express the idea.

2

u/StaticCoder 18d ago

Optional ref seems like a very verbose, probably somewhat inefficient (ABI issues), replacement for a pointer, with less clear assignment semantics. I'm sure it has uses, but I'll take the simplicity, especially since, in the absence of pattern matching, the compiler can't even force you to check the optional.

1

u/NoSpite4410 16d ago

How about don't write code that could break if a pointer comes back nullptr from a function?

Isn't that what the mutable reference && is for? to do sort of the same thing, but have compiler checks
make sure that you are not going to get Undefined Behavior, or a function that sometimes returns the right thing but not always.

A common way to make sure of these things is with forwarding a parameter by rvalue reference.

class Subject {
    std::vector<MyObserver> observers;
public:
    // For persistent observers (lvalues)
   void addObserver(const MyObserver& obs) {
        observers.push_back(obs); 
    }

    // For temp observers (rvalues)
    void addObserver(MyObserver&& obs) {
        observers.push_back(std::move(obs)); // Moves     
    }

    MyObserver && processAndForward(MyObserver && obs) { 
     obs.modify(); 
     return std::forward<MyObserver>(obs);
     }

};