C++ Logo

std-proposals

Advanced search

Re: Add a specialized "Extension" concept to the inheritance syntax

From: Ofri Sadowsky <sadowsky.o.phd_at_[hidden]>
Date: Mon, 20 May 2019 23:23:10 +0300
Continuing from my previous message to Mark, here's yet another refinement
of the mechanism.

Assume we have this structure:

class Base {
public:
    void setup() {
        try {
            setupImpl();
        }
        catch(...) {
            cleanup();
        }
    }

    void cleanup() noexcept {
        cleanupImpl();
    }

protected:
    tail_extensible void setupImpl() {
        acquireResource1();
    }

    head_extensible void cleanupImpl() noexcept {
        releaseResource1();
    }
};

class Derived : public Base
{
protected:
    void setupImpl() extension {
        acquireResource2();
    }

    void cleanupImpl() noexcept extension {
        releaseResource2();
    }
};


This code can be translated as follows.

class Base {
// the public part stays the same
protected:
    virtual void setupImpl() {
        Base_setupImpl_extension();
    }

    void Base_setupImpl_extension() {
        acquireResource1();
    }

    // similar for the cleanup
};

class Derived : public Base {
protected:
    void setupImpl() override {
        Base_setupImpl_extension();
        Derived_setupImpl_extension();
    }

    void Derived_setupImpl_extension() {
        acquireResource2();
    }

    void cleanupImpl() noexcept override {
        Derived_cleanupImpl_extension();
        Base_cleanupImpl_extension();
    }

    void Derived_cleanupImpl_extension() noexcept {
        releaseResource2();
    }
};

So, instead of calling the base class _virtual_ directly from the derived
class override, each class defines a "private" (protected here to
illustrate the feasibility) implementation of its portion of the extension
(let's call it the "extension fragment"), and then each override of the
virtual method calls a sequence of non-virtual fragment methods defined in
the base classes in top-down (tail extension) or bottom-up (head extension)
order.

In order to address the diamond pattern, the compiler would have to sort
the extension fragments in a depth-first manner (tail extension) or
reversed, and then create an override that calls the fragments in
sequence. This does not require any change in the structure of a class or
its v-table. I do expect the compiler to contain the knowledge for
sorting, because it uses it when creating constructors and destructors (for
sure, a base destructor in the diamond pattern cannot be invoked more or
less than one time).

In your example, we will have:

void A::setupImpl() override {
    D_setupImpl_extension();
    B_SetupImpl_extension();
    C_SetupImpl_extension(); //< B, C are ordered the same way as the
inheritance is declared
    A_setupImpl_extension();
}


I hope that this does demonstrate the feasibility.

It also nicely follows Sutter's recommendations as pointed to by Tony
without directly calling any virtual implementation in the base class, only
with plain override. But it really calls for automation.

As for coverage, at least in my experience, and in the context demonstrated
in the first example -- resource acquisition and release, tail and head
extension should be a fairly common pattern. It follows the c-tor/d-tor
pattern except with an actual named method. I cannot rule out other
orders, but to me there is enough ground for this form by itself.

The major question which is still open for me is actually about the return
value (a smaller question is about parameter passing). Even if
A::setupImpl() makes no use of the return values from D, B and C, they
cannot be so simply ignored. Consider the case in which setupImpl()
returns a Boolean that indicates its success or failure. Somehow, the
final return value would have to be computed. Supposedly, a natural form
is to check each extension fragment's return value and if it fails, stop
and return failure. If all the extensions are completed successfully then
return "success".

This, of course, is over-simplifying because the logic can be as complex as
we want, and the return type itself can be as complex as we want. So,
arguably, it should involve some functional object, passed as a parameter
of the extension declaration, which is applied to the return value of each
extension fragment in order to form a final unified outcome. It can
aggregate individual fragment returns, reduce them by some binary operator
(like && in the case of success indicator), or whatever.

And this does increase the complexity of the syntax etc., and should
decrease the Committee's interest in continuing to pursue the proposal.

How about requiring an _extensible_ method to take its return type as a
(const &, anyone?) parameter? Then, each fragment can decide by itself how
to deal with it and what to forward to the next fragment in line. Maybe.
Something such as:

SomeReturnType A::setupImpl(SomeReturnType const & /*, more parameters?*/)
override
{
    return
A_setupImpl_extension(C_setupImpl_extension(B_setupImpl_extension(D_setupImpl_extension
/*, ... for D?*/) /*, ... for B?*/) /*, ... for C?*/) /*, ... for A?);
}

Interim conclusion:

I think that I did demonstrate the theoretical feasibility of the head and
tail extension pattern by showing a manual translation to "conventional"
code which, in principle, can be automated. I think that the use case of
setup-acquire/cleanup-release is relevant, and that it's an interesting
pattern to learn from. Following this path, I also think that it is
feasible to develop a generalization of return-value handling through a
functional object. Of course, none of this would cover ALL the
unimaginable cases of mixing base class fragments in a derived class
override, but as long as we stay with relatively uniform patterns, I have
reason to believe that it is doable.

The down side is that the complexity of expressing head and tail extension
when return values are involved may be frighteningly large, maybe too much
for a language keyword, especially given the relatively narrow coverage of
use cases. But who knows, if we continue to look at this we might fall on
a solution that's elegant enough. Or maybe this feature can become a
library function/class instead of a keyword.

Above all, to me this _is_ an interesting discussion, and I don't want to
stop here. But to continue, I will need some encouragement :-). Will
anyone -- Matthew or other -- care to continue? If not, I'm not going to
continue writing to myself.


Thanks for all the feedback,

Ofri

On Mon, May 20, 2019 at 7:10 PM Matthew Woehlke <mwoehlke.floss_at_[hidden]>
wrote:

> On 19/05/2019 05.02, Ofri Sadowsky via Std-Proposals wrote:
> > Consider the even more complicated and, well, disagreeable, case of
> > multiple inheritance with the diamond pattern, virtual inheritance and
> > all. If the final derived class calls its two (or more) bases, and each
> in
> > turn calls the shared base, then the shared base is called twice while in
> > reality it should have been called once. An automated generation of the
> > call sequence could remedy this.
>
> But how would *that* work?
>
> Let's say we have A : B, C; B: D; C : D.
>
> In order to call D::foo() only once, the compiler would have to generate
> *two* versions of B::foo() and C::foo(); one that auto-calls D::foo()
> and one that doesn't. I think this would only work if derived methods
> *only* ever call the versions that don't auto-call bases.
>
> That said, as you later mentioned (snipped), I could see this being a
> better motivation than the more general case.
>
> > I am proposing two mechanisms for these common cases in the hope that
> this
> > is where it stops. Had there been more than two cases, then the idea
> does
> > seem somewhat futile. But IMHO these two cases are common enough and
> > important enough to look at anyhow.
>
> Again, what about the case that I need to call the base implementation
> in the middle of my derived implementation? What about the case that the
> method returns something, and the derived implementation needs to use or
> modify that return value?
>
> I'm concerned that pure-head and pure-tail only cover a fraction of use
> cases. I'm also concerned that the rest of the committee will feel that
> the work of adding a language feature does not justify the purported
> benefit, when a compiler warning would achieve most of the benefit with
> far less cost.
>
> --
> Matthew
>


-- 
Ofri Sadowsky, PhD
Computer Science Consulting and Training
7 Carmel St., #37
Rehovot  76305
Israel
Tel: +972-77-3436003
Mob: +972-54-3113572

Received on 2019-05-20 15:25:05