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
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