C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Flat Dynamic Polymorphism Library

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Sat, 16 Jul 2022 12:36:26 -0400
On Sat, Jul 16, 2022 at 11:12 AM Nikl Kelbon via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> Thank you for your answer, I will try to answer briefly the first
> questions that have arisen
> > What you're calling "flat dynamic polymorphism" is what is typically
> referred to as "type erasure."
> No, it's implemented with type erasure, but it's not a type erasure.
>

I think the big difference between this and classic type erasure is that,
here, the thing you end up with (a `const_poly_with<Draw>` object) doesn't
itself afford a `.draw()` operation, so it's not a drop-in replacement for
the original `T`. The writer of a polymorphic function must explicitly
choose one of these two syntaxes:

// (A), static polymorphism with templates
template<class Drawable>
void option_a(const Drawable& d) {
  d.draw(std::cout); // syntax A
}

// (B), dynamic polymorphism with this new thing
void option_b(const_poly_ref<Draw> d) {
  d.invoke<Draw>(std::cout); // syntax B
}

You cannot pass a `const_poly_ref<Draw>` to `option_a`; it won't compile.
Whereas the STL types that do type erasure are specifically designed so
that they can drop-in-replace the original `T` in generic code:

// (A), static polymorphism with templates
template<class Callable>
void option_a(const Callable& f) {
  f(std::cout); // syntax A
}

// (B), dynamic polymorphism with std::function / any / unique_function /
function_ref
void option_b(std::function<void(std::ostream&)> f) {
  f(std::cout); // still syntax A (!!)
}

In general, what your design does here is actually a *good* thing —
generally, it's *helpful* to give different syntaxes to "the part you
customize in the callee" versus "the part you call from the caller."
https://quuxplusone.github.io/blog/2018/03/19/customization-points-for-functions/
One benefit you get from your current design is that it's obvious that
    Square s;
    const_poly_ref<Draw> r = s;
    any_with<Draw> a = r; // oops! error!
should not compile, because `r` does not actually afford a `.draw()`
method. (It affords only syntax B, `aa::invoke<Draw>(...)`.) On the other
hand, the Standard Library's reference_wrapper and the upcoming
function_ref permit us to write things like
    auto f = [](){};
    std::function_ref<void()> r = f;
    std::function<void()> a = r; // oops! captures a reference to `f`
instead of a copy of `f`!
    auto r2 = std::ref(f);
    std::function<void()> b = r2; // oops! captures a copy of the
reference_wrapper instead of a copy of `f`!
because `r` and `r2` both *do* afford the syntax `r()`. So you get into a
whole mess about what ought to happen when you try to implicitly convert a
function_ref into a function_ref, or a function into a slightly different
function, or a function pointer into a function_ref...
    int f();
    std::function<int()> a = f;
    std::function<long()> b = a; // this std::function type-erases a copy
of `a`, which type-erases a pointer to `f`
    std::function<int()> c = b; // this std::function type-erases a copy
of `b`, which type-erases a copy of `a`, which type-erases a pointer to `f`
    int (*pf)() = f;
    std::function_ref<int()> d = pf; // does this capture a reference to
`pf`, or a reference to `f` itself?
    pf = g;
    d(); // does this call `g`, or `f`?
With your library design, you don't run into any of these questions.
`const_poly_ref<A>` does not implicitly convert to `any_with<A>`, nor even
to `const_poly_ref<SlightlyDifferentA>`. This makes it less ergonomic to
use (IMHO), but it does sidestep a lot of irritating philosophical issues.


What about examples, pdf presents some links to godbolt with full examples
> and my version of dyno 'Drawable' here :
> https://godbolt.org/z/8YxPncEqE
>

FWIW, that was very useful to me. :)
However, I still doubt the overall idea here. It's cool that the programmer
can get almost-type-erasure in several different flavors (poly_ref,
const_poly_ref, any_with,...) with just those 6 lines of boilerplate. But
if the programmer knows *which* flavor they want, they can get that
specific flavor in <20 lines of boilerplate today — not significantly more,
in my opinion — and they can make their type a drop-in replacement for the
original `T`, too, so they don't have to use the weird `aa::invoke<Draw>`
syntax.
https://godbolt.org/z/zqPhxndjK
But observe that this objection/skepticism of mine isn't at all specific to
*your* library; I'd probably say the same thing about Dyno and
Boost.TypeErasure and the rest. Your library might fill the same niche for
people who are already in the target audience of those libraries. I just
continue to fail to understand who those people are.

my $.02,
Arthur

Received on 2022-07-16 16:36:38