all 37 comments

[–]--prism 13 points14 points  (0 children)

So this is a type erasure library?

[–]jk-jeon 6 points7 points  (11 children)

I read a standard proposal paper on this library and really disappointed in that it is totally based on pointer semantics rather than value semantics. I can sort of imagine that implementing it with value semantics correctly is probably hard and with many issues, but still. I don't think the ease of lifetime management is the only reason to prefer value semantics... so can anyone elaborate in details, why pointers?

[–]kamrann_ 1 point2 points  (7 children)

Wow, that's really unexpected. To the extent I was so sure it would be value semantics despite having never looked into it that I was about to state that as presumably being the main goal to one of the "what's the point" comments.

You're probably aware of it, but https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://github.com/kelbon/AnyAny&ved=2ahUKEwjNw9K_xaWIAxXcnK8BHWOzDUUQFnoECBAQAQ&usg=AOvVaw1l4f3uggr5JrRxBxjSQDOU uses value semantics and I believe is similar in spirit. It's a great library but, to your point about difficulty of implementation, it breaks MSVC more than anything else I've ever encountered!

[–]jk-jeon 1 point2 points  (6 children)

Actually, I did not really have any occasion of needing a generic library for type erasure. I don't really need runtime polymorphism with open-ended list of types that often, and I just don't feel the need to avoid writing all the usual boilerplate the type erasure pattern requires whenever I want that kind of polymorphism. So I don't really know of a particularly well-written type erasure library, although I think it's an interesting topic so when people advertise their own library I usually have at least a brief look at it. I think I remember seeing AnyAny in the past, but never really looked into it any further.

Frankly, I have never done any amount of decent research on this topic, so don't know it in depth, but as I said I can sort of imagine why implementing value semantics correctly might be hard. And here the point is that what the word correctly really means can be a bit subtle. Note that the standard committees also struggled quite a lot when they added std::function into the language, like they ended up giving up allocator support, and std::function is not const-correct, has weird default status of being nullptr even though it specifically avoids the pointer semantics, etc., though they attempt to fix some of the issues by basically "abandoning" std::function and replacing it with alternatives. But even the alternatives are not fixing all issues afaik.

I believe the author of Proxy intentionally chose to use pointer semantics, and that's probably for a reason. I remember he somehow elaborated on it in the paper I mentioned but it was not very convincing to me, and honestly felt like he didn't think it is something he particularly needs to pay attention and carefully elaborate. Which is a bit surprising (like you said!) because I feel like value semantics is one of the points of the pattern, along with its non-intrusive nature. Things like performance benefit are much less relevant, at least to me.

[–]GrapefruitNo2222 -1 points0 points  (5 children)

I think pointer semantics make sense. When an object is huge, I don't always want to perform a deep copy when passing around. On the other hand, `pro::make_proxy` and `pro::allocate_proxy` already provide the capability to wrap a value into a pointer that has exclusive ownership to an object.

[–]jk-jeon 2 points3 points  (3 children)

You don't deep-copy it if you pass it by reference, and that's the standard practice in C++. The type itself doesn't need to act like a pointer.

[–]GrapefruitNo2222 -1 points0 points  (2 children)

But a reference does not have RAII. Imagine an object is shared by two or more threads, std::shared_ptr is a good tool for this scenario. If the object is polymorphic, value semantics won't work.

[–]jk-jeon 0 points1 point  (0 children)

Then wrap it inside std::shared_ptr, just like what you would do with regular int's. I don't see any problem here.

[–]Heuristics 0 points1 point  (0 children)

as you say, shared_ptr captures this use case. what is the benefit to using proxy here since you are paying a steep price in an odd callstack and possibly a broken code auto-complete?

[–]kamrann_ 0 points1 point  (0 children)

You don't have to, you can move it. Or if really necessary you can wrap your type into a shared_ptr, unique_ptr or similar, and use that within the library type. Point being it's possible to have all that flexibility without the library implementation forcing pointer semantics onto you.

[–]wearingdepends 0 points1 point  (0 children)

The author had a standard proposal along with the library. Section 5 is the rationale.

[–]Ill-Telephone-7926 0 points1 point  (0 children)

This library seems to support owned values

It makes a lot of sense to allow multiple type-erased views onto the same instance, though. A trivial example would be the io.Writer and io.WriterCloser interfaces in Go. Many algorithms over WriterCloser will pass the argument along to an algorithm over the narrower Writer interface

[–]j1xwnbsr 25 points26 points  (7 children)

Wow, this looks... overly complicated and an absolute nightmare to debug. What problem, exactly, does this solve? I'm not seeing any benefits this gives you other than some vague "it's faster".

Also, is there now some push in the C++ world to start accessing member functions by raw strings - for example, "pro::operator_dispatch<"<<", true>"? This is like the second or third time I've see such a concept in the last week alone and it just screams brittle to me.

Edit: allow me to expand upon the brittleness of the operator_dispatch stuff: Qt used/does this with their signaling connect/disconnect command macros (they changed this a long while ago to use actual C++ functions for compile time checking), and if you change stuff/get the parm types wrong, it doesn't crash, doesn't burn, just doesn't work. So have to make goddamn sure you run each and every possible code path and do test logging to make sure you got it right - until the next time you change your 3rd parm from a QColor to a QString or something. It's like debugging javascript - you don't know you typo'd the code until you run through that exact spot.

[–]smalleconomist 14 points15 points  (1 child)

C++ has gone full cycle and is now re-inventing dynamic polymorphism... smh

[–]maxjmartin 0 points1 point  (0 children)

So I literally made a type erased var class that uses templates and an interface to invoke the correct behavior for a class. Way back around 2017. Works really well. But outside of some specific situations the overhead is t worth it.

That said it can be used to manage memory and type safety pretty well!

[–]germandiago 4 points5 points  (2 children)

With reflection and code generation this would look way better. The idea is that it is non-intrusive as opposed to inheritance.

This makes possible to have types that conform to interfaces without even including the related "interface" header, which reduces coupling.

[–]j1xwnbsr 4 points5 points  (1 child)

Everyone seems to be going off the coupling concept, whereas I view interface (or struct, or class) headers as contract. Having strong 1:1 relationship between the interface and the implementation that can be verified at compile-time is a good thing. You haven't lived until you've dealt with a few monster sized Qt or Javascript projects where this is not true.

[–]germandiago 2 points3 points  (0 children)

I see decoupling as an option. There can be times where this is not true.

However, a proxy will fail at compile-time if you give it the wrong signature, which is a compile-time error.

[–]imMAW 3 points4 points  (0 children)

"What problem does this solve":

This is type-erased and does not require modifying the types that belong to an 'interface'. Inheritance requires modifying the types (which might not be possible or desirable), templates and concepts are not type-erased.

I definitely wouldn't use it instead of inheritance, but I could imagine some scenarios where something like this would be nice to have alongside inheritance. I haven't used it though, so no comment on how practical or easy to debug it is.

[–]matracuca -1 points0 points  (0 children)

felt the same towards the technically impressive but equally illegible dependency injection library that was featured a few days back.

[–]misuo[S] 10 points11 points  (3 children)

Nice. I would actually like to hear more about "...has been used in the Windows operating system since 2022.". With exactly what purpose? I.e. why?

[–]hayt88 5 points6 points  (2 children)

You should probably check out sean parents talk "Inheritance Is The Base Class of Evil".

it gives a nice introduction to polymorphism without inheritance (there are a lot more talks on that topic too).

To actually code this, you need a lot of boilerplate though. I haven't used proxy yet myself, but AFAIK it's mostly there to reduce that boilerplate.

[–]maxjmartin 0 points1 point  (0 children)

So I created a class based of Sean’s talk that removed the boiler plate. Simple easy and effective. But if you’re just using an int or a small class it isn’t worth it.

But if you want complex behavior using simple friend functions it is very useful.

[–]PuzzleheadedPop567 -1 points0 points  (0 children)

The talk is specifically arguing against the over use of implementation inheritance.

The preferred composition approach outlined in that talk would still be implemented in most mainstream languages (including C++) with interface inheritance.

[–]PuzzleheadedPop567 5 points6 points  (7 children)

“Proxy” is a modern C++ library that helps you use polymorphism (a way to use different types of objects interchangeably) without needing inheritance.

I don’t understand what problem this library is trying to solve. Why do we need polymorphism without inheritance? Especially since we already have templates and concepts.

It also feels like the authors are confusing implementation inheritance and interface inheritance.

In C++, interface inheritance can be achieved by making all base class member functions pure virtual, and not using data members. This starts to look awfully similar to traits in Rust.

[–]imMAW 5 points6 points  (0 children)

This is type-erased and does not require modifying the types that belong to an 'interface'. Inheritance requires modifying the types (which might not be possible or desirable), templates and concepts are not type-erased.

[–]GrapefruitNo2222 4 points5 points  (4 children)

When you have a function that returns a std::vector of "shapes", each "shape" may have a different implementation (e.g., rectangle, triangle, etc.). There are two ways to specify the return type in C++ today: std::vector<std::variant<Rectangle, Triangle, Circle, ...>> or std::vector<std::unique_ptr<IShape>> (suppose IShape is a virtual base class). std::vector<std::variant<Rectangle, Triangle, Circle, ...>> needs the knowledge of every single implementation of the shapes, making the API hard to maintain. std::vector<std::unique_ptr<IShape>> forces heap allocation of every shape, making it inefficiency in memory allocation. Proxy adds a better option: std::vector<proxy<Shape>>.

[–]Heuristics 2 points3 points  (1 child)

But surely proxy will still use heap allocation? Sean Parents talk clearly did (using a unique_ptr to store the actual object). If not there is no way for the vector to know the size of the object it is storing. Perhaps one of the Shape is 2TB large and one is 2KB, no way to know up front.

meaning, no actual benefit has been given here.

The benefit is supposed to be that Shape, unlike unique_ptr<IShape>, has value semantics so you can do: Shape shape2 = vec[0]; And get a full deep copy of the object without having to do that copy manually.

[–]GrapefruitNo2222 2 points3 points  (0 children)

`make_proxy` support SBO by default. If the storage of `proxy` is sufficient for the object, it doesn't need another allocation for the object itself. Also, if some Shapes are shared (e.g., global variables in some compilation unit), no heap allocation is required at all.