Skip to content

Narrowing protocols with reflection #98

Description

@RyanJK5

The draft notes that narrowing should be allowed for protocols:

// Narrowing constructor from any compatible mutable protocol_view.
template <typename Other>
protocol_view(const protocol_view<Other>& other) noexcept;

This is an idea worth exploring with reflection. I would like to provide a brief overview of duck's approach to this problem, and the issues we may encounter when implementing this for protocol.

duck's model

Rather than accepting a single trait (same as protocol's interface), duck accepts a variadic number of traits. A duck<A, B, C> can then be converted to a duck<A>, duck<B>, etc.

// where A, B, C are interfaces
rjk::duck<A, B, C> d{...};
rjk::duck_view<A> dv1 = d;
rjk::duck_view<B> dv2 = d;
rjk::duck_view<const C> dv3 = d;

The approach we take for this looks like:

struct vtable<A, B, C> {
    const vtable<A>* to_trait_0;
    const vtable<B>* to_trait_1;
    const vtable<C>* to_trait_2;

    // actual dispatch functions, lifecycle, etc.
};

// for single-trait vtables
struct vtable<A> {
    const vtable<const A>* to_const;
};

I am currently working on a feature for the library I call "multi-trait" narrowing, or converting from a duck<A, B, C> to a duck<A, B>, duck<A, C>, or any other subset of the traits.

With the approach I'm using currently (and the best one I could think of so far) this means that a vtable<A, B, C> will now need to store a vtable<A, B>*, vtable<A, C>*, vtable<B, C>*, in addition to the single-trait vtable pointers. As you imagine, this could become a problem for a lot of traits. It is not, however, an issue for a large number of interface members, since duck does not allow granular narrowing to each method.

protocol's model

Bringing this info to protocol, if I understood the specification correctly, the goal is to support the following:

struct A {
    int foo();
    int bar();
    int baz();
};

struct B {
    int foo();
    int bar();
};

protocol<A> p{...};
protocol_view<B> pv1 = p;
protocol_view<const B> pv2 = p;

My first thought for implementing this would be to reuse the approach from duck, but treating each interface member as a "trait" instead. So you would make three vtables: one for int foo(), one for int bar(), and one for int baz(). Then each one would need to store a pointer to the vtable for its constituent members. So a vtable for A might contain:

struct vtable<foo, bar, baz> {
    const vtable<foo>* to_func_0;
    const vtable<bar>* to_func_1;
    const vtable<baz>* to_func_2;
    const vtable<foo, bar>* to_func_0_1;
    const vtable<foo, baz>* to_func_0_2;
    const vtable<bar, baz>* to_func_1_2;

    // actual dispatch functions, lifecycle, etc.
};

If we are anticipating interfaces with a large number of methods, this could slow down compile time significantly. Defining one interface with many members, even if the user does not intend to narrow it at all, would cause significant overhead.

Conclusion

I am not creating this to discourage narrowing from being included in protocol, since I think it is a high-value feature. Some additional research into alternate methods using reflection would be helpful, so as to potentially avoid the overhead described above. Additionally, it would be valuable to discuss this with STL implementers to see if they have access to some methods that we don't.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions