C++ Logo

std-proposals

Advanced search

Re: [std-proposals] this return type

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Fri, 7 Apr 2023 19:53:10 -0400
On Fri, Apr 7, 2023 at 6:37 PM Jan Schultke <janschultke_at_[hidden]>
wrote:

> > IIUC, your example intends that the `this` keyword would be used both in
> `Base` and also in `B`, is that right?
> > https://www.godbolt.org/z/xWdcaoxE6
> Yes, that is correct, the Compiler Explorer link is what I envision.
> [...]
> Keep in mind that returning this is exactly equivalent to
> returning a reference to the self-type, with qualifications applied.
> Without the extra feature of making return statements implicit, this
> proposal could be implemented via [[returns_this]] annotation (where
> the program is IFNDR if we don't return a self-reference).
> [...]
> (4) The program would be ill-formed if a return statement in a
> this-returning function is not of the form return *this;
> However, this approach is *very* fishy if the function is
> rvalue-reference qualified. It would require ignoring that the
> returned reference cannot normally bind to the return type of the
> function.
>

Hm, this raises two additional questions!
(4a) You say that you want to make the program ill-formed (hard error) if
there's an unexpected-looking return statement in a `this`-returning
function.
But consider these situations:
    struct A {
        this f();
        this g();
        this h1(bool b) { if (b) return f(); }
        this h2(bool b) { if (b) return f(); return g(); }
    };
    struct B {
        B& f(); // perhaps inherited from a base class
        B& g(); // perhaps inherited from a base class
        this h1(bool b) { if (b) return f(); }
        this h2(bool b) { if (b) return f(); return g(); }
    };
In `A`, we could postulate "okay, `return f()` isn't unexpected because
`f()` itself is a `this`-returning expression." But that would be a special
case; yuck. And in `B`, we can't even postulate that.
Alternatively, we could say that `B::h1` is ill-formed and the programmer
should just rewrite it as
    this h1(bool b) { if (b) (void)f(); }
But then the programmer would be tempted to rewrite `B::h2` as
    this h2(bool b) { if (b) (void)f(); (void)g(); }
and *that* is an *in*correct transformation. (The correct transformation
would replace `return` with `else`.) Forbidding early returns won't fly in
committee, I'm sure of it.
So I don't think you can make unexpected returns into hard errors. I think
they have to be UB-at-runtime-if-executed.

(4b) In your answer to (4) you bring up rvalue-ref-qualified methods. I
hadn't thought of that — that's very good! But I guess I'm also confused
as to what solution you have in mind. Consider:
    struct R1 {
        R1& f() { return *this; } // OK
        R1& g1() && { return *this; } // OK
        R1&& g2() && { return std::move(*this); } // OK
        const R1& h() const { return *this; } // OK
    };
    struct R2 {
        this f() { } // OK, returns R2&
        this g() && { } // OK, returns R2& or R2&&; or hard error?
        this h() const { } // OK, returns const R2& ?
    };
I had tacitly assumed that `R2::h` would be well-formed and return `const
R2&`. But that should have been one of my explicit questions.
I hadn't thought about `R2::g` at all. I suppose I would have assumed it
worked exactly like `R1::g1`. But that *is* a little weird, because it
says: "If *this is const-qualified, the return type should be
const-qualified. But if *this is &&-qualified, the return type should *not*
be &&-qualified."
Perhaps `R2::g` should work like `R1::g2`. That is, `this f() *cvref-quals*`
should invariably return `*cvref-qualified*(T)`, which implies that when
cvref-quals includes `&&` the implicitly added return statement is of the
form `return std::move(*this);`, not `return *this;`. (But it's *implicitly*
added, so the programmer doesn't really have to think about it.)

[...] But what I fear is that developers get accustomed to implicit return
> statements and forget them more frequently when downgrading to an
> older standard.
>

I'm not worried about that, because (AFAIK) all compilers make the warning
for forgetting a return statement default-on (even without `-Wall`). Even
if it's inside a template, the only way you won't see a warning for
    template<class T>
    struct S {
        S& operator=(const S&) { }
    };
is if you have literally zero unit-tests that instantiate that operator, or
if you compile your tests with `-w`. Both are solidly in the "programmer's
fault" category for me.

Cheers,
Arthur

Received on 2023-04-07 23:53:23