C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Chimeric Pointer

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Fri, 25 Nov 2022 11:45:41 -0500
On Fri, Nov 25, 2022 at 9:49 AM Jason McKesson via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> There seems to be a broad lack of understanding of what this feature
> is doing [...] The goal of the proposal is to be able to write a function
> that takes
> a single pointer to any type which inherits from *two or more* base
> classes. And then use that pointer to access any function which is
> available from any of these base classes. [...]
>
> Personally, I don't think this feature is worth doing. The
> concept/template solution is adequate for those who need it, and it's
> probably not something that is *broadly* useful. It's a niche feature.
>

Ditto, but my reason for not-adding-this-feature-to-C++ wouldn't be "it's
niche" so much as "you don't need a core-language feature for this; you can
already accomplish your goal just by using the existing language."
Consider again the example [slightly simplified]:

    struct Control { virtual void SetBackgroundColor(); };
    struct TextEntry { virtual void SetValue(const char*); };
    struct TextCtrl1 : Control, TextEntry {};
    struct TextCtrl2 : Control, TextEntry {};

    void Red(??? *p) {
        p->SetBackgroundColor();
        p->SetValue("pending");
    }
    int main() {
        TextCtrl1 tc1;
        TextCtrl2 tc2;
        Red(&tc1);
        Red(&tc2);
    }

Frederick's programming problem is: How do we write function `Red` so that
it can accept either TextCtrl1 or TextCtrl2, and yet without making `Red`
itself a function template? (We don't want `Red` to be a template because
that would force its whole definition to be defined in a header file, which
means recursively including all the stuff *used* in that definition, and so
on.)

Frederick's suggested solution was to just make it work by magic:

    void Red(__chimerical(Control, TextEntry) *p) {
        p->SetBackgroundColor(); // this means Control::SetBackgroundColor
        p->SetValue("pending"); // this means TextEntry::SetValue
    }

However, this has problems. What would happen if the programmer wrote this
code, and then later the maintainer of wxWidgets changed the definition of
`Control` as follows?
    struct Control { void SetBackgroundColor(); void SetValue(const char*);
};
Now:
    p->SetValue("pending"); // Does this still mean TextEntry::SetValue?
if so, why? Is it now ambiguous and ill-formed?
This is related to the guideline "Don't inherit from types you don't
control." This magic __chimerical(X,Y) becomes a method of "ad-hoc
inheritance," by which any client programmer can introduce an
inheritance-like dependency coupling the interfaces of any two classes X
and Y.
If you were proposing to implement the magic behavior by changing name
lookup, then you also have to explain whether
    struct Control { void SetBackgroundColor(); void SetValue(); };
would be treated similarly: Is `p->SetValue("pending")` still ambiguous, or
do we do overload resolution among all the possible meanings of `SetValue`
and pick the best match, or what?

Anyway, you don't need to change the language for this. There are
well-established programming techniques to accomplish your goal.
The simplest would be to completely open-code it:

    // https://godbolt.org/z/Eq49Tqa9f
    void Red(void *p) {
        struct Dummy { virtual ~Dummy(); };
        auto asControl = [](void *p) {
            return dynamic_cast<Control*>(static_cast<Dummy*>(p));
        };
        auto asTextEntry = [](void *p) {
            return dynamic_cast<TextEntry*>(static_cast<Dummy*>(p));
        };
        asControl(p)->SetBackgroundColor();
        asTextEntry(p)->SetValue("pending");
    }

This `Red` accepts a pointer to anything inheriting from both Control and
TextEntry. Of course it also accepts a pointer to anything else (e.g. an
`int`), and has UB if you do that; but, don't do that. You can check for
that by tossing a simple template on the front end:

    template<class T>
    void Red(T *p) {
        static_assert(is_base_of_v<Control, T> && is_base_of_v<TextEntry,
T>);
        RedImpl(p); // calls RedImpl(void*) as above
    }

So that's the first option that exists today. The second option is to
introduce your own "TextCtrl" interface, either as a base class (if you
control the hierarchy at some point above `TextCtrl1` and `TextCtrl2`):

    // https://godbolt.org/z/nMhj1r13e
    struct TextCtrl : Control, TextEntry {};
    struct TextCtrl1 : TextCtrl {}; // CHANGED!!
    struct TextCtrl2 : TextCtrl {}; // CHANGED!!
    void Red(TextCtrl *p) {
        p->SetBackgroundColor();
        p->SetValue("pending");
    }

or using type erasure (if you don't control the hierarchy):

    // https://godbolt.org/z/38qvM8T64
    struct TextCtrlPtr {
        TextEntry *t_;
        Control *c_;
        template<class T> TextCtrlPtr(T *p) : t_(p), c_(p) {}
        TextEntry *asTextEntry() const { return t_; }
        Control *asControl() const { return c_; }
    };
    void Red(TextCtrlPtr p) {
        p.asControl()->SetBackgroundColor();
        p.asTextEntry()->SetValue("pending");
    }

Obviously you can polish `TextCtrlPtr` by making `SetBackgroundColor` and
`SetValue` into member functions of it, if you really want to.
For certain heavy-OOP-design-pattern-ish applications you might even want
to introduce an adaptor class of your own. Here `TextCtrl1` and `TextCtrl2`
are out of our control, but we can write a base class we *want* to use
(`TextCtrl`) and then a concrete adaptor class `TextCtrlAdaptor` that can
wrap a pointer to any type that implements both original base classes. This
is the same as the type-erasure approach, except that the inheritance is
explicit, and the `TextCtrlAdaptor` object actually IS-A `TextCtrl`. This
is the first implementation where the `virtual` keyword really matters to
the correctness of the example.

    struct TextCtrl1 : Control, TextEntry {};
    struct TextCtrl2 : Control, TextEntry {};
    struct TextCtrl : Control, TextEntry {};
    struct TextCtrlAdaptor : TextCtrl {
        TextEntry *t_;
        Control *c_;
        template<class T> explicit TextCtrlAdaptor(T *p) : t_(p), c_(p) {}
        void SetBackgroundColor() override { c_->SetBackgroundColor(); }
// overrides virtual Control::SetBackgroundColor
        void SetValue(const char *s) override { t_->SetValue(s); } //
overrides virtual TextEntry::SetValue
    };
    void Red(TextCtrl *p) {
        p->SetBackgroundColor();
        p->SetValue("pending");
    }
    int main() {
        TextCtrl1 tc1;
        TextCtrl2 tc2;
        TextCtrlAdaptor ta1(&tc1); Red(&ta1);
        TextCtrlAdaptor ta2(&tc2); Red(&ta2);
    }

HTH,
Arthur

Received on 2022-11-25 16:45:54