From f904fe87f6cbd8d27150e7004bb71cf959f41e7c Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Thu, 6 Aug 2020 01:09:19 -0400 Subject: [PATCH] Sketch out static polymorphism in C++ --- _posts/2020-08-05-static-polymorphism.md | 182 +++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 _posts/2020-08-05-static-polymorphism.md diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md new file mode 100644 index 0000000..ab7e9f6 --- /dev/null +++ b/_posts/2020-08-05-static-polymorphism.md @@ -0,0 +1,182 @@ +--- +layout: post +title: "Static Polymorphism" +description: "Emulating Traits in C++" +category: +tags: [python] +--- + +# Simple Example + +Accept parameter types, return known type. + +# Generic return + +Same parameter signature, but return different types - `AsRef` + +# Associated types + +`.as_iter()`, and the iterator item types + +# Arbitrary `self` + +[`std::enable_shared_from_this`](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this) + +`enable_unique_from_this` doesn't make a whole lot of sense, but Rust can do it: + +```rust +struct MyStruct {} + +impl MyStruct { + fn my_function(self: &Box) {} +} + +fn main() { + let unboxed = MyStruct {}; + // error[E0599]: no method named `my_function` found for struct `MyStruct` in the current scope + // unboxed.my_function(); + + let boxed = Box::new(MyStruct {}); + boxed.my_function(); + boxed.my_function(); +} +``` + +Interestingly enough, can't bind `static` version using equality: + +```c++ +#include +#include +#include + +std::uint64_t free_get_value() { + return 24; +} + +class MyClass { +public: + // :11:47: error: invalid pure specifier (only '= 0' is allowed) before ';' token + std::uint64_t get_value() = free_get_value; +}; + +int main() { + auto x = MyClass {}; +} +``` + +--- + +Turns out the purpose of `enable_shared_from_this` is so that you can create new shared instances of +yourself from within yourself, it doesn't have anything to do with enabling extra functionality +depending on whether you're owned by a shared pointer. _At best_, you could have other runtime +checks to see if you're owned exclusively, or as part of some other smart pointer, but the type +system can't enforce that. And if you're _not_ owned by that smart pointer, what then? Exceptions? + +UFCS would be able to help with this - define new methods like: + +```c++ +template<> +void do_a_thing(std::unique_ptr value) {} +``` + +In this case, the extension is actually on `unique_ptr`, but the overload resolution applies only to +pointers of `MyType`. Note that `shared_ptr` and others seem to work by overloading `operator ->` to +proxy function calls to the delegates; you could inherit `std::shared_ptr` and specialize the +template to add methods for specific classes I guess? But it's still inheriting `shared_ptr`, you +can't define things directly on it. + +Generally, "you can just use free functions" seems like a shoddy explanation. We could standardize +overload `MyClass_init` as a constructor, etc., but the language is designed to assist us so we +don't have to do crap like that. I do hope UFCS becomes a thing. + +# Default implementation + +First: example of same name, different arguments. Not possible in Rust. + +Can you bind a free function in a non-static way? Pseudocode: + +```c++ +template +concept DoMethod = requires (T a) { + { a.do_method(std::declval() } -> std::same_as; + { a.do_method() } -> std::same_as; +} + +template requires DoMethod +std::uint64_t free_do_method(T& a) { + a.do_method(0); +} + +class MyClass { +public: + std::uint64_t do_method(std::uint64_t value) { + return value * 2; + } + + // Because the free function still needs a "this" reference (unlike Javascript which has a + // floating `this`), we can't bind as `std::uint64_t do_method() = free_do_method` + std::uint64_t do_method() { + return free_do_method(this); + } +}; +``` + +# Require concept methods to take `const this`? + +# Move/consume `self` as opposed to `&self`? + +Is there a way to force `std::move(object).method()`? + +# Require static methods on a class? + +# `override`, or other means of verifying a function implements a requirement? + +# Local trait implementation of remote types? + +AKA "extension methods". UFCS can accomplish this, and could use free functions to handle instead, +but having the IDE auto-complete `.` is exceedingly useful, as opposed to memorizing +what functions are necessary for conversion. We're not changing what's possible, just making it +easier for humans. + +Likely requires sub-classing the remote class. Implicit conversions don't _really_ work because they +must be defined on the remote type. + +Rust makes this weird because you have to `use ClientExt` to bring the methods in scope, but the +trait name might not show up because `impl ClientExt for RemoteStruct` is defined elsewhere. +Alternately, `ClientExt: AnotherTrait` implementations where the default `ClientExt` implementation +is used. + +# Automatic markers? + +Alternately, conditional inheritance based on templates? + +# Trait objects as arguments + +```rust +trait MyTrait { + fn some_method(&self); +} + +fn my_function(value: &dyn MyTrait) { + +} +``` + +C++ can't explicitly use vtable as part of concepts: + +```c++ +template> +void my_function(T& value) {} +``` + +...is equivalent to: + +```rust +fn my_function(value: &T) {} +``` + +vtable is automatically used if declared virtual. + +`dyn Trait` seems to be used in Rust mostly for type erasure - `Box>` for example, +but is generally fairly rare, and C++ probably doesn't suffer for not having it. Can use inheritance +to force virtual if truly necessary, but not sure why you'd need that.