Date: Wed, 22 Jan 2025 11:15:11 -0500
On Wed, Jan 22, 2025 at 10:48 AM Federico Kircheis via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> On 22 January 2025 15:29:38 UTC, Dennis Gusev via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
> >If you look up "inheriting from STL containers", the top results from SO
> >and reddit all mention it being a bad practice since standard containers
> >aren't designed to be inherited from.
> >
> >Specifically, they dont have virtual destructors so there's a risk of UB
> >from deleting through pointer to base.
>
That's not the reason. Even "slicing" isn't the biggest reason. The biggest
reason IMO is that "STL types are like a box of chocolates: you never know
what you're going to get." The API of a standard library type tends to
change in subtle ways with every release, not to mention varying in subtle
ways across different vendors. There's no sensible way to "extend" an
interface that may change out from under you.
https://quuxplusone.github.io/blog/2018/12/11/dont-inherit-from-std-types/
> If you think you *do* know “what you get” when your type T inherits from
> std::vector: quick, does your type T have a member function ==, and what
> does it do? Does it have emplace_back, and if so, what is emplace_back’s
> return type? Does it have CTAD deduction guides? And so on. It’s not that
> these questions don’t *have* answers; it’s that *you* don’t know the
> answers (and neither do your coworkers).
> [...] If you (or your project, or your company) didn’t write class Foo,
> then class Foo should not be granted control over the API of your own
> class. And that’s what you’re doing when you inherit from a base class:
> you’re granting that class control over your API.
What you *can* safely do is "composition, not inheritance." Take the STL
type and make it a *member*, not a base. Re-expose exactly the API you care
about, through an overload set of member functions that you control.
> Protected and public inheritance.
> Are they not enough?
>
Protected and private inheritance allow you to somewhat-disable the
derived-to-base conversion, but:
(1) they don't actually take it away, they just make it *inaccessible*,
which can have unwanted effects on overload resolution;
(2) they also make-inaccessible all of the member functions, member types,
etc., of the base class. My understanding is that Dennis wants to keep the
members accessible, so that you can do e.g. `sonOfVector.size()` and
`sonOfVector[i]` but forbid `vector<int>(sonOfVector)`.
This goal will feel much more impossible when you realize that C++23's
"explicit object parameter" functions allow us to write not only
vector(const vector& other); // Dennis wants to prevent passing a
SonOfVector to that const vector& parameter
but also
size_t size(this const vector& self); // But Dennis *doesn't* want to
prevent passing a SonOfVector to *that* const vector& parameter!
This is related to the perennial impossibility of "strong typedefs."
<https://quuxplusone.github.io/blog/2018/06/12/perennial-impossibilities/#strong-typedefs>
–Arthur
std-proposals_at_[hidden]> wrote:
> On 22 January 2025 15:29:38 UTC, Dennis Gusev via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
> >If you look up "inheriting from STL containers", the top results from SO
> >and reddit all mention it being a bad practice since standard containers
> >aren't designed to be inherited from.
> >
> >Specifically, they dont have virtual destructors so there's a risk of UB
> >from deleting through pointer to base.
>
That's not the reason. Even "slicing" isn't the biggest reason. The biggest
reason IMO is that "STL types are like a box of chocolates: you never know
what you're going to get." The API of a standard library type tends to
change in subtle ways with every release, not to mention varying in subtle
ways across different vendors. There's no sensible way to "extend" an
interface that may change out from under you.
https://quuxplusone.github.io/blog/2018/12/11/dont-inherit-from-std-types/
> If you think you *do* know “what you get” when your type T inherits from
> std::vector: quick, does your type T have a member function ==, and what
> does it do? Does it have emplace_back, and if so, what is emplace_back’s
> return type? Does it have CTAD deduction guides? And so on. It’s not that
> these questions don’t *have* answers; it’s that *you* don’t know the
> answers (and neither do your coworkers).
> [...] If you (or your project, or your company) didn’t write class Foo,
> then class Foo should not be granted control over the API of your own
> class. And that’s what you’re doing when you inherit from a base class:
> you’re granting that class control over your API.
What you *can* safely do is "composition, not inheritance." Take the STL
type and make it a *member*, not a base. Re-expose exactly the API you care
about, through an overload set of member functions that you control.
> Protected and public inheritance.
> Are they not enough?
>
Protected and private inheritance allow you to somewhat-disable the
derived-to-base conversion, but:
(1) they don't actually take it away, they just make it *inaccessible*,
which can have unwanted effects on overload resolution;
(2) they also make-inaccessible all of the member functions, member types,
etc., of the base class. My understanding is that Dennis wants to keep the
members accessible, so that you can do e.g. `sonOfVector.size()` and
`sonOfVector[i]` but forbid `vector<int>(sonOfVector)`.
This goal will feel much more impossible when you realize that C++23's
"explicit object parameter" functions allow us to write not only
vector(const vector& other); // Dennis wants to prevent passing a
SonOfVector to that const vector& parameter
but also
size_t size(this const vector& self); // But Dennis *doesn't* want to
prevent passing a SonOfVector to *that* const vector& parameter!
This is related to the perennial impossibility of "strong typedefs."
<https://quuxplusone.github.io/blog/2018/06/12/perennial-impossibilities/#strong-typedefs>
–Arthur
Received on 2025-01-22 16:15:24