mirror of
				https://github.com/bspeice/speice.io
				synced 2025-11-03 10:00:37 -05:00 
			
		
		
		
	Sketch out static polymorphism in C++
This commit is contained in:
		
							
								
								
									
										182
									
								
								_posts/2020-08-05-static-polymorphism.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								_posts/2020-08-05-static-polymorphism.md
									
									
									
									
									
										Normal file
									
								
							@ -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<Self>) {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 <iterator>
 | 
				
			||||||
 | 
					#include <vector>
 | 
				
			||||||
 | 
					#include <concepts>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::uint64_t free_get_value() {
 | 
				
			||||||
 | 
					    return 24;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MyClass {
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
					    // <source>: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<MyType> 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<typename T>
 | 
				
			||||||
 | 
					concept DoMethod = requires (T a) {
 | 
				
			||||||
 | 
					    { a.do_method(std::declval<std::uint64_t>() } -> std::same_as<std::uint64_t>;
 | 
				
			||||||
 | 
					    { a.do_method() } -> std::same_as<std::uint64_t>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename T> requires DoMethod<T>
 | 
				
			||||||
 | 
					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 `.<the next thing>` 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<typename T, typename = std::enable_if_t<...>>
 | 
				
			||||||
 | 
					void my_function(T& value) {}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					...is equivalent to:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```rust
 | 
				
			||||||
 | 
					fn my_function<T: MyTrait>(value: &T) {}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vtable is automatically used if declared virtual.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`dyn Trait` seems to be used in Rust mostly for type erasure - `Box<Pin<dyn Future>>` 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.
 | 
				
			||||||
		Reference in New Issue
	
	Block a user