News for hackers. Stuff that matters.
Re:Care is needed to use C++ std:optional with non-trivial o... by quietbritishjim on Friday January 13, 2023 @08:36AM (#34367431)
Might as well just include that tiny snippet in your comment:

    A f(std::optional<A> z) {
        if (z) {
            return std::move(z.value());
        } else {
            return A();
        }
    }
    
    A a("message");
    auto z = std::optional<A>(std::move(a));
    f(std::move(z));

I don't think you're saying anything fundamentally different from the article. It says you need to exercise care, and you've shown an exmaple that does.

"Most of these problems can be solved with moves" - It may be that the problem of copies is solved by using moves (i.e. this is a solved problem at the compiler level). But the problem of needing to be careful is not solved by the advice to use moves (i.e. it's trickier at the human level). It's so easy to forget to use move and you would get no warning about it.

It doesn't help that the rules are very complicated. For example, if you were returning z rather than z.value() from your function (with correspondingly different return type), the usual advice is to return z rather than return std::move(z) because it's already implicitly an rvalue in that situation and using move() prevents NRVO. But... is that still true for z.value()? I don't think so but it's not obvious (IMO). Or, if you return std::move(x) where x is a const type, it will copy, which is fair enough as that's all it can do but it ought to warn you (or not compile IMO). It's not always obvious is something is const e.g. if you have a template function, x is of deduced type T (passed by copy) is const part of the deduced type? I'm not sure.

Re:Care is needed to use C++ std:optional with non-trivial o... by kllrnohj on Friday January 13, 2023 @08:43AM (#34367523)
If you're going to say "you need to be careful about copies when passing-by-value and moves are complicated" then I 100% agree. BUT if you're going to say "you need to be careful about std::optional" then I completely disagree. It's a thoroughly unsurprising class with unsurprising behavior.

Whether or not the moves do anything or not is entirely dependent on the type T here, not optional, and is therefore orthogonal to whether or not you use optional. Ie, optional did not introduce any new concerns, and therefore doesn't warrant any particular scrutiny or concern.

Re:Care is needed to use C++ std:optional with non-trivial o... by tsimionescu on Friday January 13, 2023 @09:16AM (#34368016)
I would say that std::optional's value_or() method at least is a foot gun here - as it creates a great opportunity for both an unnecessary temporary object and an unnecessary copy. That in itself merits a "use with care" flag.

Also, the fact that std::optional::value() apparently returns a reference, and thus `return opt.value()` is always forced to be a copy (I'm writing this based on another comment, I can't claim to fully understand C++'s extraordinarily complicated value types and their respective limitations) seems like another problem that's somewhat specific to std::optional itself.

Re:Care is needed to use C++ std:optional with non-trivial o... by exitb on Friday January 13, 2023 @09:27AM (#34368164)
::value() method can both return a reference (bare use) and give up the ownership of the wrapped value (used with std::move). You just have to state your intention correctly. It's not a problem with std::optional specifically, it's just the default C++ behavior. The entire thing works much better in Rust, where move is the default op and copies need to be explicit.
Re:Care is needed to use C++ std:optional with non-trivial o... by kllrnohj on Friday January 13, 2023 @09:28AM (#34368174)
`return opt.value()` is not forced to be a copy, no. It is, as you noted, a reference return. You can of course continue to propagate that as a reference if you want, assuming that makes sense.

I don't think I'd agree with a characterization that `value_or` is a footgun? It behaves basically identically to similar features in every other language (like Java's Optional#orElse). If it somehow changed parameter evaluation such that it was deferred then that would be a footgun. But it's of course not doing that.

Re:Care is needed to use C++ std:optional with non-trivial o... by tsimionescu on Friday January 13, 2023 @10:49AM (#34369395)
I'm calling value_or a footgun because it calls both the initial constructor (which is to be expected, as in Java) but, if I understand correctly, a further copy constructor (perhaps ellided if the conditions are right). In Java/C#/Python/Ruby/Haskell/Rust etc, this copy would be guaranteed not to happen (at the cost of a heap allocation in the first 4, in most cases).

Of course, even in Java this method is often inferior - it only really makes sense in languages like Haskell or if replaced by a macro of some kind, which would guarantee that the unnecessary value is neither constructed nor copied. Otherwise, an if/else or ? should generally be preferred, especially in a performance-focused language like C++.

Re:Care is needed to use C++ std:optional with non-trivial o... by kllrnohj on Friday January 13, 2023 @11:11AM (#34369764)
https://en.cppreference.com/w/cpp/utility/optional/value_or

value_or's parameter type is U&&, so no there's no additional copy there. There's only the explicitly visible default constructor call.

But you did get me to take a second look at the post, and yeah the author's statement of "Finally, we copy construct an instance of A when the call to value_or terminates." is just incorrect. Try it out yourself: https://godbolt.org/z/hWqnrfGKE

If you replace the `.value_or(A())` with `.value()` there's no reduction in copies, only a single reduction in the constructed count which of course makes sense since the `A()` parameter isn't there.

The way you would optimize this to eliminate copies would be to do things like construct the optional in-place and take the parameter by const& instead, such as:

   A f(const std::optional<A>& z) {
      return z.value_or(A());
   }

   A g() { 
      std::optional<A> z(std::in_place, "message"); 
      return f(z); 
   }
`std::in_place` being definitely a classic C++ "quirk" here and usually unlikely to be worth it but hey if you want to play "minimize the copy count" golf there you go. The value_or isn't actually adding any copies, though.
Re:Care is needed to use C++ std:optional with non-trivial o... by quietbritishjim on Friday January 13, 2023 @08:55AM (#34367723)
OK fair point
Re:Care is needed to use C++ std:optional with non-trivial o... by exitb on Friday January 13, 2023 @08:56AM (#34367725)
I agree that it's complex and overly nuanced. I posted this because the article pointed out a problem, said "be careful", but didn't actually offer any way to address it.

Just z.value() gives you a reference, which, when returned will yield a copy. So no RVO in this case and the move is the next best thing.