From f904fe87f6cbd8d27150e7004bb71cf959f41e7c Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Thu, 6 Aug 2020 01:09:19 -0400 Subject: [PATCH 01/25] 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. From fc83163b7173114ea52c561580ecc07ca2315a64 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Mon, 10 Aug 2020 11:16:08 -0400 Subject: [PATCH 02/25] Minor tweak --- _posts/2020-08-05-static-polymorphism.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index ab7e9f6..0661ba8 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -144,7 +144,8 @@ 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. +is used. To do this, Rust compiles the entire crate as a single translation unit, and the orphan +rule. # Automatic markers? From 0bb4772d03a28be157340cfda1b8d35b8d2a4568 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Fri, 21 Aug 2020 15:12:29 -0400 Subject: [PATCH 03/25] Continuing to flesh out --- _posts/2020-08-05-static-polymorphism.md | 59 ++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 0661ba8..e41f49b 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -86,8 +86,23 @@ template to add methods for specific classes I guess? But it's still inheriting 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. +overload `MyClass_init` as a constructor and function similar to C, 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. + +That said, it is interesting that for Rust, arbitrary self can be replaced with traits: + +```rust +trait MyTrait { + fn my_function(&self); +} + +impl MyTrait for Box { + fn my_function(&self) {} +} +``` + +Just have to make sure that `MyTrait` is in scope all the time, and that's not fun. Ultimately, Rust +kinda already has UFCS. # Default implementation @@ -115,6 +130,7 @@ public: // 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` + // Also can't do it because it's a syntax error; can only use `= 0` to indicate pure virtual. std::uint64_t do_method() { return free_do_method(this); } @@ -123,14 +139,21 @@ public: # Require concept methods to take `const this`? +`std::is_const` should be able to handle it: https://en.cppreference.com/w/cpp/types/is_const + # Move/consume `self` as opposed to `&self`? -Is there a way to force `std::move(object).method()`? +Is there a way to force `std::move(object).method()`? C++ can still use objects after movement makes +them invalid, so not sure that it makes conceptual sense. # Require static methods on a class? +Shouldn't be too hard - `T::some_method()` should be compilable. + # `override`, or other means of verifying a function implements a requirement? +`noexcept`, etc. + # Local trait implementation of remote types? AKA "extension methods". UFCS can accomplish this, and could use free functions to handle instead, @@ -176,7 +199,35 @@ void my_function(T& value) {} fn my_function(value: &T) {} ``` -vtable is automatically used if declared virtual. +Alternate form with concepts: + +```c++ +#include +#include + +template +concept HasMethod = requires (T a) { + { a.some_method() } -> std::same_as; +}; + +auto my_function(HasMethod auto value) { + auto x = value.some_method(); +} + +class MyClass { +public: + std::uint64_t some_method() { + return 42; + } +}; + +int main() { + auto x = MyClass {}; + my_function(x); +} +``` + +vtable is automatically used if virtual, but concepts (so far as I can tell) can't detect 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 From 304df4ff1b10c7ba805a5db8048213906da580ad Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Mon, 24 Aug 2020 14:00:03 -0400 Subject: [PATCH 04/25] Continuing to flesh this out --- _posts/2020-08-05-static-polymorphism.md | 65 ++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index e41f49b..e7b02f4 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -6,6 +6,9 @@ category: tags: [python] --- +Other languages have done similar things (interfaces in Java), but think the Rust comparison is +useful because both languages are "system." + # Simple Example Accept parameter types, return known type. @@ -20,6 +23,8 @@ Same parameter signature, but return different types - `AsRef` # Arbitrary `self` +Forms the basis for Rust's async system, but used very rarely aside from that. + [`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: @@ -102,7 +107,8 @@ impl MyTrait for Box { ``` Just have to make sure that `MyTrait` is in scope all the time, and that's not fun. Ultimately, Rust -kinda already has UFCS. +kinda already has UFCS. It's only "kinda" because you have to bring it in scope, and it's +potentially unclear when it's being used (extension traits), but it does get the basic job done. # Default implementation @@ -154,7 +160,7 @@ Shouldn't be too hard - `T::some_method()` should be compilable. `noexcept`, etc. -# Local trait implementation of remote types? +# Local trait implementation of remote data type? 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 @@ -162,7 +168,60 @@ what functions are necessary for conversion. We're not changing what's possible, 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. +must be defined on the remote type (not true: `operator Local` must be defined on remote, but +`Local` could have a `Local(const Remote&)` implicit constructor). Could maybe use wrapper classes +that have single-arg (implicit) constructors, and get away with it as long as the wrapper knows it's +not safe to modify the internals. That said, wrapper can only use the public interface unless +declared friend (which is no different to Rust). + +```c++ +#include +#include + +class SomeRemoteClass {}; + +template +concept MyConcept = requires (T a) { + { a.do_something() } -> std::same_as; +}; + +// Note: It's unsafe to move `SomeRemoteClass`, so we accept by reference +// Requiring SomeRemoteClass be copy-constructible would also be OK. +class LocalImpl { +public: + LocalImpl(const SomeRemoteClass &remote): remote_{remote} {}; + std::uint64_t do_something() { + return 42; + } + +private: + const SomeRemoteClass &remote_; +}; + +auto auto_func(MyConcept auto value) { + auto x = value.do_something(); +} + +void regular_func(LocalImpl value) { + auto x = value.do_something(); +} + +int main() { + SomeRemoteClass x {}; + + // This isn't OK because `auto` doesn't automatically convert to `LocalImpl` + //auto_func(x); + + // This _is_ OK because we explicitly declare the class we want (`LocalImpl`) and `SomeRemoteClass` + // is implicitly converted. Just so happens that `LocalImpl` implements `MyConcept`. + regular_func(x); + + // We could extend the conversion pattern using specializations of `LocalImpl`, or maybe use + // `std::variant` to hold different internal types, but there's still a disconnect between + // what we actually want to fulfill (`MyConcept`) and how that's implemented for remote types + // (using the `LocalImpl` wrapper and implicit conversions). +} +``` 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. From a62c6851d93f19d3d55bd5017e2dc93d27b3a567 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Tue, 25 Aug 2020 00:20:23 -0400 Subject: [PATCH 05/25] Continue working on this --- _posts/2020-08-05-static-polymorphism.md | 77 ++++++++++++++++++++---- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index e7b02f4..5ed1350 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -11,15 +11,19 @@ useful because both languages are "system." # Simple Example -Accept parameter types, return known type. +Accept parameter types, return known type. Also needs to be generic over parameter types. # Generic return -Same parameter signature, but return different types - `AsRef` +Same name and parameter signature, but return different types - `AsRef` # Associated types -`.as_iter()`, and the iterator item types +`.as_iter()`, and the iterator `Item` type + +# Require static methods on a class? + +Shouldn't be too hard - `T::some_method()` should be compilable. # Arbitrary `self` @@ -149,16 +153,9 @@ public: # Move/consume `self` as opposed to `&self`? -Is there a way to force `std::move(object).method()`? C++ can still use objects after movement makes -them invalid, so not sure that it makes conceptual sense. - -# Require static methods on a class? - -Shouldn't be too hard - `T::some_method()` should be compilable. - -# `override`, or other means of verifying a function implements a requirement? - -`noexcept`, etc. +Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force +`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not +sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. # Local trait implementation of remote data type? @@ -288,6 +285,60 @@ int main() { vtable is automatically used if virtual, but concepts (so far as I can tell) can't detect virtual. +Kind of nice because you don't have to explicitly manage the vtable in Rust, but you trade off the +ability to get inheritance. Modern trends have been "composition over inheritance" (see Google style +docs as an example) so the trend may be worth it, but moving away from inheritance models is +disorienting. + `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. + +# Checking a type fulfills the concept + +With concepts, you find out that there's an issue only when you attempt to use it. Traits in Rust +will let you know during implementation that something is wrong (there's a local error). +https://www.ecorax.net/as-above-so-below-1/ + +Can use `static_assert` to kinda make sure a contract is fulfilled: + +```c++ +#include +#include + +template +constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; + +class WithMethod { +public: + std::uint64_t method() { return 0; } +}; + +static_assert(has_method); + +class WithoutMethod {}; + +// : In instantiation of 'constexpr const bool has_method': +// :16:16: required from here +// :5:71: error: 'class WithoutMethod' has no member named 'method' +// 5 | constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; +// | ~~~~~~~~~~~~~~~~~~^~~~~~ +// :16:15: error: non-constant condition for static assertion +// 16 | static_assert(has_method); +// | +static_assert(has_method); +``` + +We'd rather the example fail the static assert, rather than have an error on the `decltype`, but it +does get the job done; we're told explicitly that `WithoutMethod` has no member `method`, so the +error message for `decltype()` is actually much nicer than the `static_assert`.. Can use +[custom SFINAE](https://stackoverflow.com/a/257382) or +[experimental](https://stackoverflow.com/a/22014784) +[type traits](http://en.cppreference.com/w/cpp/experimental/is_detected) to fix those issues, but +mostly please just use concepts. + +# Visibility + +Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that +Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where +C++ concepts are purely duck typing. From 92a706415ae765fb1b8098a8e42bf8ed7b78a0ee Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Tue, 25 Aug 2020 12:01:26 -0400 Subject: [PATCH 06/25] Some parts aren't specifically static polymorphism, but are more direct comparisons to Rust traits (which form the basis of Rust static polymorphism) --- _posts/2020-08-05-static-polymorphism.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 5ed1350..d41646f 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -7,7 +7,8 @@ tags: [python] --- Other languages have done similar things (interfaces in Java), but think the Rust comparison is -useful because both languages are "system." +useful because both languages are "system." Mostly looking at how static polymorphism is implemented +in C++ and Rust, but also some comparisons to Rust behavior not strictly related to polymorphism. # Simple Example From 75fab4c80ddb8225f2cb2038c2881735c490dc76 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Fri, 28 Aug 2020 13:30:15 -0400 Subject: [PATCH 07/25] Decltype --- _posts/2020-08-05-static-polymorphism.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index d41646f..e014f25 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -343,3 +343,10 @@ mostly please just use concepts. Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where C++ concepts are purely duck typing. + +# decltype and compiler-named types + +Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` +from traits. Can return `impl Future` from free functions and structs, but traits can't use +compiler-generated types (associated types still need to name the type). C++ doesn't appear to have +the same restrictions. From ee39f7bc430fb7d09c823230c2ce77c8f179a26d Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Fri, 28 Aug 2020 13:32:05 -0400 Subject: [PATCH 08/25] Change the decltype order --- _posts/2020-08-05-static-polymorphism.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index e014f25..0b72173 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -22,6 +22,13 @@ Same name and parameter signature, but return different types - `AsRef` `.as_iter()`, and the iterator `Item` type +# decltype and compiler-named types + +Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` +from traits. Can return `impl Future` from free functions and structs, but traits can't use +compiler-generated types (associated types still need to name the type). C++ doesn't appear to have +the same restrictions. + # Require static methods on a class? Shouldn't be too hard - `T::some_method()` should be compilable. @@ -343,10 +350,3 @@ mostly please just use concepts. Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where C++ concepts are purely duck typing. - -# decltype and compiler-named types - -Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` -from traits. Can return `impl Future` from free functions and structs, but traits can't use -compiler-generated types (associated types still need to name the type). C++ doesn't appear to have -the same restrictions. From 0ad37c23e016d736197bf6f027e9b05bc509af57 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Fri, 28 Aug 2020 15:24:34 -0400 Subject: [PATCH 09/25] Forcing methods to use const --- _posts/2020-08-05-static-polymorphism.md | 91 +++++++++++++++++++++--- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 0b72173..1684835 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -26,8 +26,12 @@ Same name and parameter signature, but return different types - `AsRef` Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` from traits. Can return `impl Future` from free functions and structs, but traits can't use -compiler-generated types (associated types still need to name the type). C++ doesn't appear to have -the same restrictions. +compiler-generated types (associated types still need to name the type). Can have traits return +references (and use vtable, so no longer statically polymorphic), but typically get into all sorts +of lifetime issues. Can also use `Box` trait objects to avoid lifetime issues, but again, uses +vtable. + +C++ doesn't appear to have the same restrictions, mostly because the "contract" is just duck typing. # Require static methods on a class? @@ -159,11 +163,67 @@ public: `std::is_const` should be able to handle it: https://en.cppreference.com/w/cpp/types/is_const -# Move/consume `self` as opposed to `&self`? +--- -Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force -`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not -sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. +`is_const` could be used to declare the entire class is const, but don't think you could require +const-ness for only certain methods. Can use `const_cast` to assert "constness" though: + +```c++ +#include +#include + +template +concept ConstMethod = requires (T a) { + { const_cast(a).method() } -> std::same_as; +}; + +std::uint64_t my_function(ConstMethod auto a) { + return a.method(); +} + +class HasConst { +public: + std::uint64_t method() const { + return 42; + } +}; + +class WithoutConst { +public: + std::uint64_t method() { + return 42; + } +}; + +int main() { + auto x = HasConst{}; + my_function(x); + + auto y = WithoutConst{}; + my_function(y); +} +``` + +```text +:32:18: error: use of function 'uint64_t my_function(auto:1) [with auto:1 = WithoutConst; uint64_t = long unsigned int]' with unsatisfied constraints + 32 | my_function(y); + | ^ +:9:15: note: declared here + 9 | std::uint64_t my_function(ConstMethod auto a) { + | ^~~~~~~~~~~ +:9:15: note: constraints not satisfied +: In instantiation of 'uint64_t my_function(auto:1) [with auto:1 = WithoutConst; uint64_t = long unsigned int]': +:32:18: required from here +:5:9: required for the satisfaction of 'ConstMethod' [with auto:1 = WithoutConst] +:5:23: in requirements with 'T a' [with T = WithoutConst] +:6:37: note: the required expression 'const_cast(a).method()' is invalid, because + 6 | { const_cast(a).method() } -> std::same_as; + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~ +:6:37: error: passing 'const WithoutConst' as 'this' argument discards qualifiers [-fpermissive] +:22:19: note: in call to 'uint64_t WithoutConst::method()' + 22 | std::uint64_t method() { + | ^~~~~~ +``` # Local trait implementation of remote data type? @@ -234,10 +294,6 @@ Alternately, `ClientExt: AnotherTrait` implementations where the default `Client is used. To do this, Rust compiles the entire crate as a single translation unit, and the orphan rule. -# Automatic markers? - -Alternately, conditional inheritance based on templates? - # Trait objects as arguments ```rust @@ -350,3 +406,18 @@ mostly please just use concepts. Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where C++ concepts are purely duck typing. + +# Potentially excluded + +Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth +investigating in a future post? + +## Move/consume `self` as opposed to `&self`? + +Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force +`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not +sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. + +## Automatic markers? + +Alternately, conditional inheritance based on templates? From e48b2f5abb54edda953602c634417d7b94bed858 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 19:59:04 -0400 Subject: [PATCH 10/25] Further reorganization --- _posts/2020-08-05-static-polymorphism.md | 404 +++++++++++++---------- 1 file changed, 226 insertions(+), 178 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 1684835..c8b6712 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -7,8 +7,15 @@ tags: [python] --- Other languages have done similar things (interfaces in Java), but think the Rust comparison is -useful because both languages are "system." Mostly looking at how static polymorphism is implemented -in C++ and Rust, but also some comparisons to Rust behavior not strictly related to polymorphism. +useful because both languages are "system." + +# System Differences + +Worth noting differences in goals: polymorphism in C++ is only duck typing. Means that static +polymorphism happens separate from visibility, overloading, etc. + +Rust's trait system is different (need a better way to explain that) which allows for trait markers, +auto-deriving, arbitrary self. # Simple Example @@ -26,10 +33,11 @@ Same name and parameter signature, but return different types - `AsRef` Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` from traits. Can return `impl Future` from free functions and structs, but traits can't use -compiler-generated types (associated types still need to name the type). Can have traits return -references (and use vtable, so no longer statically polymorphic), but typically get into all sorts -of lifetime issues. Can also use `Box` trait objects to avoid lifetime issues, but again, uses -vtable. +compiler-generated types (associated types still need to name the type). + +Can have traits return references (`&dyn Trait`), but uses vtable (so no longer statically +polymorphic), and very likely get into all sorts of lifetime issues. Can use `Box` trait +objects to avoid lifetime issues, but again, uses vtable. C++ doesn't appear to have the same restrictions, mostly because the "contract" is just duck typing. @@ -37,126 +45,70 @@ C++ doesn't appear to have the same restrictions, mostly because the "contract" Shouldn't be too hard - `T::some_method()` should be compilable. -# Arbitrary `self` - -Forms the basis for Rust's async system, but used very rarely aside from that. - -[`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 and function similar to C, 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. - -That said, it is interesting that for Rust, arbitrary self can be replaced with traits: - -```rust -trait MyTrait { - fn my_function(&self); -} - -impl MyTrait for Box { - fn my_function(&self) {} -} -``` - -Just have to make sure that `MyTrait` is in scope all the time, and that's not fun. Ultimately, Rust -kinda already has UFCS. It's only "kinda" because you have to bring it in scope, and it's -potentially unclear when it's being used (extension traits), but it does get the basic job done. - # 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: +```rust +trait MyTrait { + // This is illegal in Rust, even though name-mangling is unique: + // fn method(&self, value: usize) -> usize; + + // Works if you rename the method, but is a pain to type: + fn method_with_options(&self, value: usize) -> usize; + fn method(&self) -> usize { + self.method_with_options(42); + } +} + +struct MyStruct {} +impl MyTrait for MyStruct { + fn method_with_options(&self, value: usize) -> usize { + println!("{}", value); + value + } +} +``` + +Second: example of same name, different arguments, but can't provide default implementation. ```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); +template +concept MyTrait = requires (T a) { + { a.method(declval()) } -> std::same_as, + { a.method() } -> std::same_as, } +// Each class must implement both `method` signatures. class MyClass { public: - std::uint64_t do_method(std::uint64_t value) { - return value * 2; + std::size_t method(std::size_t value) { + std::cout << value << std::endl; + return value; } - // 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` - // Also can't do it because it's a syntax error; can only use `= 0` to indicate pure virtual. - std::uint64_t do_method() { - return free_do_method(this); + std::size_t method() { + return method(42); } }; + +// Can write free functions as the default and then call explicitly, but for trivial +// implementations (replacing defaults) it's not likely to be worth it. +auto method_default_(auto MyTrait this) std::size_t { + return this.method(42); +} + +class MyClassDefault { +public: + std::size_t method(std::size_t value) { + std::cout << value << std::endl; + return value; + } + + std::size_t method() { + return method_default_(this); + } +} ``` # Require concept methods to take `const this`? @@ -165,8 +117,9 @@ public: --- -`is_const` could be used to declare the entire class is const, but don't think you could require -const-ness for only certain methods. Can use `const_cast` to assert "constness" though: +`is_const` could be used to declare the class is const for an entire concept, but don't think you +could require const-ness for only certain methods. Can use `const_cast` to assert "constness" +though: ```c++ #include @@ -294,8 +247,167 @@ Alternately, `ClientExt: AnotherTrait` implementations where the default `Client is used. To do this, Rust compiles the entire crate as a single translation unit, and the orphan rule. +Rust can do one thing special though - can run methods on literals - `42.my_method()`. + +# Checking a type fulfills the concept + +With concepts, you find out that there's an issue only when you attempt to use it. Traits in Rust +will let you know during implementation that something is wrong (there's a local error). +https://www.ecorax.net/as-above-so-below-1/ + +Can use `static_assert` to kinda make sure a contract is fulfilled: + +```c++ +#include +#include + +template +constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; + +class WithMethod { +public: + std::uint64_t method() { return 0; } +}; + +static_assert(has_method); + +class WithoutMethod {}; + +// : In instantiation of 'constexpr const bool has_method': +// :16:16: required from here +// :5:71: error: 'class WithoutMethod' has no member named 'method' +// 5 | constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; +// | ~~~~~~~~~~~~~~~~~~^~~~~~ +// :16:15: error: non-constant condition for static assertion +// 16 | static_assert(has_method); +// | +static_assert(has_method); +``` + +We'd rather the example fail the static assert, rather than have an error on the `decltype`, but it +does get the job done; we're told explicitly that `WithoutMethod` has no member `method`, so the +error message for `decltype()` is actually much nicer than the `static_assert`.. Can use +[custom SFINAE](https://stackoverflow.com/a/257382) or +[experimental](https://stackoverflow.com/a/22014784) +[type traits](http://en.cppreference.com/w/cpp/experimental/is_detected) to fix those issues, but +mostly please just use concepts. + +# Potentially excluded + +Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth +investigating in a future post? + +## Visibility + +Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that +Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where +C++ concepts are purely duck typing. + +## Move/consume `self` as opposed to `&self`? + +Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force +`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not +sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. + +## Automatic markers? + +Alternately, conditional inheritance based on templates? + +## Arbitrary `self` + +Handled as part of section on `impl Trait` for remote type, not sure this needs it's own section. + +Forms the basis for Rust's async system, but used very rarely aside from that. + +[`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 and function similar to C, 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. + +That said, it is interesting that for Rust, arbitrary self can be replaced with traits: + +```rust +trait MyTrait { + fn my_function(&self); +} + +impl MyTrait for Box { + fn my_function(&self) {} +} +``` + +Just have to make sure that `MyTrait` is in scope all the time, and that's not fun. Ultimately, Rust +kinda already has UFCS. It's only "kinda" because you have to bring it in scope, and it's +potentially unclear when it's being used (extension traits), but it does get the basic job done. + # Trait objects as arguments +Handled as part of `decltype` and compiler-named types, not sure it needs it's own section. + ```rust trait MyTrait { fn some_method(&self); @@ -357,67 +469,3 @@ disorienting. `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. - -# Checking a type fulfills the concept - -With concepts, you find out that there's an issue only when you attempt to use it. Traits in Rust -will let you know during implementation that something is wrong (there's a local error). -https://www.ecorax.net/as-above-so-below-1/ - -Can use `static_assert` to kinda make sure a contract is fulfilled: - -```c++ -#include -#include - -template -constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; - -class WithMethod { -public: - std::uint64_t method() { return 0; } -}; - -static_assert(has_method); - -class WithoutMethod {}; - -// : In instantiation of 'constexpr const bool has_method': -// :16:16: required from here -// :5:71: error: 'class WithoutMethod' has no member named 'method' -// 5 | constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; -// | ~~~~~~~~~~~~~~~~~~^~~~~~ -// :16:15: error: non-constant condition for static assertion -// 16 | static_assert(has_method); -// | -static_assert(has_method); -``` - -We'd rather the example fail the static assert, rather than have an error on the `decltype`, but it -does get the job done; we're told explicitly that `WithoutMethod` has no member `method`, so the -error message for `decltype()` is actually much nicer than the `static_assert`.. Can use -[custom SFINAE](https://stackoverflow.com/a/257382) or -[experimental](https://stackoverflow.com/a/22014784) -[type traits](http://en.cppreference.com/w/cpp/experimental/is_detected) to fix those issues, but -mostly please just use concepts. - -# Visibility - -Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that -Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where -C++ concepts are purely duck typing. - -# Potentially excluded - -Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth -investigating in a future post? - -## Move/consume `self` as opposed to `&self`? - -Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force -`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not -sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. - -## Automatic markers? - -Alternately, conditional inheritance based on templates? From e24cc4c7a5269fe11ba253041290eb74dd694c58 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 21:16:57 -0400 Subject: [PATCH 11/25] Further refining remote types --- _posts/2020-08-05-static-polymorphism.md | 74 ++++++++++++++++-------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index c8b6712..7daff0e 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -178,19 +178,19 @@ int main() { | ^~~~~~ ``` -# Local trait implementation of remote data type? +# Implement methods on 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. +Rust allows both arbitrary `self` and extension traits. Arbitrary self forms the basis of the +`async` system in Rust. Extension traits form basis of `futures` library. Accomplish effectively the +same thing, but for concrete types and traits respectively. -Likely requires sub-classing the remote class. Implicit conversions don't _really_ work because they -must be defined on the remote type (not true: `operator Local` must be defined on remote, but -`Local` could have a `Local(const Remote&)` implicit constructor). Could maybe use wrapper classes -that have single-arg (implicit) constructors, and get away with it as long as the wrapper knows it's -not safe to modify the internals. That said, wrapper can only use the public interface unless -declared friend (which is no different to Rust). +UFCS would achieve the same effect, but unclear if/when it will be available: +https://dancrn.com/2020/08/02/ufcs-in-clang.html + +Can use free functions in the meantime, but having the IDE auto-complete `.` is +exceedingly useful, as opposed to looking through all functions in a namespace. + +Can also sub-class or implicitly convert to a wrapper: ```c++ #include @@ -227,27 +227,51 @@ void regular_func(LocalImpl value) { int main() { SomeRemoteClass x {}; - // This isn't OK because `auto` doesn't automatically convert to `LocalImpl` + // This _will not_ compile because `auto` doesn't trigger the conversion to `LocalImpl` //auto_func(x); - // This _is_ OK because we explicitly declare the class we want (`LocalImpl`) and `SomeRemoteClass` - // is implicitly converted. Just so happens that `LocalImpl` implements `MyConcept`. + // This _will_ compile because the function signature declares a concrete class for which an + // implicit conversion is available. It just so happens that `LocalImpl` satisfies `MyConcept`. regular_func(x); - - // We could extend the conversion pattern using specializations of `LocalImpl`, or maybe use - // `std::variant` to hold different internal types, but there's still a disconnect between - // what we actually want to fulfill (`MyConcept`) and how that's implemented for remote types - // (using the `LocalImpl` wrapper and implicit conversions). } ``` -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. To do this, Rust compiles the entire crate as a single translation unit, and the orphan -rule. +The `LocalImpl` wrapper could be extended to handle additional remote types using template +specialization or holding an internal `std::variant`, but that misses the point: we want to write +code that accepts anything that satisfies `MyConcept`. When we write functions that require a +specific wrapper, we're being overly restrictive, and obfuscating our intentions (we don't actually +care about the wrapper, it's just there for ease-of-use). -Rust can do one thing special though - can run methods on literals - `42.my_method()`. +Can use some overloading/specialization tricks for ease of use: + +```c++ +auto some_func_(MyConcept auto value) -> void { + auto x = value.do_something(); +} + +auto some_func(MyConcept auto value) -> void { + some_func_(value); +} + +void some_func(LocalImpl value) { + some_func_(value); +} +``` + +Need to be careful though: + +```c++ +auto some_func(MyConcept auto value) -> void { + auto x = value.do_something(); +} + +void some_func(LocalImpl value) { + // NOTE: This is actually a recursive call because `LocalImpl` is more specific than `auto`, + // so will overflow the stack. + // We use `some_func_` above to uniquely name the function we actually want to call. + some_func(value); +} +``` # Checking a type fulfills the concept From 3709bcd0fd8c3c44aab4de8c4bac1703c55c00a1 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 21:19:49 -0400 Subject: [PATCH 12/25] Orphan rule --- _posts/2020-08-05-static-polymorphism.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 7daff0e..52ceb48 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -266,13 +266,16 @@ auto some_func(MyConcept auto value) -> void { } void some_func(LocalImpl value) { - // NOTE: This is actually a recursive call because `LocalImpl` is more specific than `auto`, - // so will overflow the stack. + // NOTE: Because `LocalImpl` is more specific than `auto`, this is a recursive call and + // will overflow the stack. // We use `some_func_` above to uniquely name the function we actually want to call. some_func(value); } ``` +Potentially worth mentioning orphan rule in Rust as limit to extension methods - can't implement +remote traits for remote types. + # Checking a type fulfills the concept With concepts, you find out that there's an issue only when you attempt to use it. Traits in Rust From 92f24c50ee4a95e90e782c9a961f6905b42bd611 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 21:49:17 -0400 Subject: [PATCH 13/25] Const-ness of method arguments as well --- _posts/2020-08-05-static-polymorphism.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 52ceb48..739c73d 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -111,15 +111,11 @@ public: } ``` -# Require concept methods to take `const this`? +# Method Qualifiers -`std::is_const` should be able to handle it: https://en.cppreference.com/w/cpp/types/is_const +Rust allows declaring immutable, mutable, and consumed arguments (including `self`). ---- - -`is_const` could be used to declare the class is const for an entire concept, but don't think you -could require const-ness for only certain methods. Can use `const_cast` to assert "constness" -though: +C++ can use `const_cast` to assert "constness" of `this` and method arguments: ```c++ #include @@ -128,6 +124,7 @@ though: template concept ConstMethod = requires (T a) { { const_cast(a).method() } -> std::same_as; + { a.another(std::declval()) } -> std::same_as; }; std::uint64_t my_function(ConstMethod auto a) { @@ -139,6 +136,11 @@ public: std::uint64_t method() const { return 42; } + + // NOTE: non-`const` value is also acceptable here. + std::uint64_t another(const std::uint64_t value) { + return value; + } }; class WithoutConst { @@ -146,6 +148,10 @@ public: std::uint64_t method() { return 42; } + + std::uint64_t another(const std::uint64_t value) { + return value; + } }; int main() { @@ -178,6 +184,9 @@ int main() { | ^~~~~~ ``` +...but difficult to do anything beyond that. Is there a way to declare methods must be `noexcept`, +`volatile`, etc.? Also can't have methods that consume `this`. + # Implement methods on remote types Rust allows both arbitrary `self` and extension traits. Arbitrary self forms the basis of the From cc5ca25fa54bdea11c3f8714cdedb6938ad77c7c Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 29 Aug 2020 22:35:25 -0400 Subject: [PATCH 14/25] Method qualifiers are weirder than I thought. --- _posts/2020-08-05-static-polymorphism.md | 101 ++++++++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 739c73d..b1ad58f 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -115,7 +115,7 @@ public: Rust allows declaring immutable, mutable, and consumed arguments (including `self`). -C++ can use `const_cast` to assert "constness" of `this` and method arguments: +C++ can use `const_cast` to assert "constness" of `this`: ```c++ #include @@ -124,7 +124,6 @@ C++ can use `const_cast` to assert "constness" of `this` and method arguments: template concept ConstMethod = requires (T a) { { const_cast(a).method() } -> std::same_as; - { a.another(std::declval()) } -> std::same_as; }; std::uint64_t my_function(ConstMethod auto a) { @@ -136,11 +135,6 @@ public: std::uint64_t method() const { return 42; } - - // NOTE: non-`const` value is also acceptable here. - std::uint64_t another(const std::uint64_t value) { - return value; - } }; class WithoutConst { @@ -148,10 +142,6 @@ public: std::uint64_t method() { return 42; } - - std::uint64_t another(const std::uint64_t value) { - return value; - } }; int main() { @@ -184,8 +174,91 @@ int main() { | ^~~~~~ ``` -...but difficult to do anything beyond that. Is there a way to declare methods must be `noexcept`, -`volatile`, etc.? Also can't have methods that consume `this`. +...but can't mark `this` as consumed. + +Working with `const` parameters can be a bit weird because of implicit copies: + +```c++ +#include +#include + +class WithCopyCtor { +public: + WithCopyCtor(const WithCopyCtor &other) = default; +}; + +class WithoutCopyCtor { +public: + WithoutCopyCtor(const WithoutCopyCtor &other) = delete; +}; + +template +concept ConstArgument = requires (T a) { + // Arguments passed by value: + { a.method_one(std::declval()) } -> std::same_as; + { a.method_two(std::declval()) } -> std::same_as; + + // Arguments passed by reference: + { a.method_three(std::declval()) } -> std::same_as; + { a.method_four(std::declval()) } -> std::same_as; + + // NOTE: This requirement is illogical. It's impossible to call a method accepting a parameter + // by value when that parameter can not copy construct. + // Not sure if it's worth including this note in the final write-up though. + //{ a.method_four(std::declval()) } -> std::same_as; + + { a.method_five(std::declval()) } -> std::same_as; +}; + +std::uint64_t my_function(ConstArgument auto a) { + return 42; +} + +class MyClass { +public: + // NOTE: Even though the concept required `method_one` to accept `const std::uint64_t`, we don't need + // to use a `const` qualifier here because we can implicitly copy `const std::uint64_t` to `std::uint64_t`. + std::uint64_t method_one(std::uint64_t value) { + return 42; + } + + // NOTE: Similar to `method_one`, even though the concept declared `const WithCopyCtor`, + // we can use the copy constructor to implicitly copy and convert between `const` and non-`const`. + std::uint64_t method_two(WithCopyCtor value) { + return 42; + } + + // NOTE: Because we can't implicitly copy from `const` references to non-`const` references, + // _even if the class has a copy constructor_, we must include the qualifier here. + std::uint64_t method_three(const WithCopyCtor &value) { + return 42; + } + + // NOTE: Similar to `method_three`, because we can't copy from `const` rvalue references to non-`const`, + // we must include the qualifier. + std::uint64_t method_four(const WithoutCopyCtor &&value) { + return 42; + } + + // NOTE: We can _add_ a `const` qualifier even if the concept doesn't require it, because it's safe to + // treat non-`const` references as `const. + std::uint64_t method_five(const WithoutCopyCtor &value) { + return 42; + } +}; + +int main() { + auto x = MyClass{}; + my_function(x); +} +``` + +Rust is much simpler about all this - the signature for a trait implementation must _exactly_ match +a trait definition. + +C++ also has way more qualifiers - `noexcept`, `override`, `volatile`, but I can't find a way to +require those qualifiers being present. In contrast Rust doesn't have exceptions, doesn't have +inheritance, and uses `unsafe` to handle `volatile`, so doesn't need to care about these qualifiers. # Implement methods on remote types @@ -341,6 +414,8 @@ C++ concepts are purely duck typing. ## Move/consume `self` as opposed to `&self`? +Handled as part of method qualifiers. + Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force `std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. From fb29d8c9daee569a2f4a99114260b9017a1d56c7 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 01:55:54 -0400 Subject: [PATCH 15/25] Add `noexcept` and `volatile` --- _posts/2020-08-05-static-polymorphism.md | 186 ++++++++++++++++++++++- 1 file changed, 179 insertions(+), 7 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index b1ad58f..f2ecb4e 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -14,13 +14,15 @@ useful because both languages are "system." Worth noting differences in goals: polymorphism in C++ is only duck typing. Means that static polymorphism happens separate from visibility, overloading, etc. -Rust's trait system is different (need a better way to explain that) which allows for trait markers, -auto-deriving, arbitrary self. +Rust's trait system is more thorough (need a better way to explain that), which allows for trait +markers, auto-deriving, arbitrary self. # Simple Example Accept parameter types, return known type. Also needs to be generic over parameter types. +Should make a quick note that C++ doesn't allow + # Generic return Same name and parameter signature, but return different types - `AsRef` @@ -113,7 +115,7 @@ public: # Method Qualifiers -Rust allows declaring immutable, mutable, and consumed arguments (including `self`). +Rust allows declaring immutable or mutable. C++ can use `const_cast` to assert "constness" of `this`: @@ -254,11 +256,174 @@ int main() { ``` Rust is much simpler about all this - the signature for a trait implementation must _exactly_ match -a trait definition. +a trait definition. Actual usage rules may be weird (what happens with a mut reference +`#[derive(Copy)]` struct when a function takes immutable by value?), but the polymorphic side stays +consistent. -C++ also has way more qualifiers - `noexcept`, `override`, `volatile`, but I can't find a way to -require those qualifiers being present. In contrast Rust doesn't have exceptions, doesn't have -inheritance, and uses `unsafe` to handle `volatile`, so doesn't need to care about these qualifiers. +Can also use `noexcept` qualifier. Not sure why this has issues: + +```c++ +#include +#include + +template +concept NoExceptMethod = requires (T a) { + { noexcept(a.method()) } -> std::same_as; +}; + +class NoExcept { +public: + std::uint64_t method() { + return 42; + } +}; + +void f(NoExceptMethod auto a) {} + +int main() { + NoExcept x{}; + + f(x); +} +``` + +Or why this is allowable: + +```c++ +#include +#include + +template +concept NoExceptMethod = requires (T a) { + { a.method() } -> std::same_as; + noexcept(a.method()); +}; + +class NoExcept { +public: + std::uint64_t method() { + return 42; + } +}; + +void f(NoExceptMethod auto a) {} + +int main() { + NoExcept x{}; + + f(x); +} +``` + +Turns out this is the way to do it: + +```c++ +#include +#include + +template +concept NoExceptMethod = requires (T a) { + { a.method() } noexcept -> std::same_as; +}; + +class NoExcept { +public: + std::uint64_t method() noexcept { + return 42; + } +}; + +void f(NoExceptMethod auto a) {} + +int main() { + NoExcept x{}; + + f(x); +} +``` + +But this doesn't compile? + +```c++ +#include +#include + +template +concept NoExceptMethod = requires (T a) { + // Note that we simply replaced `noexcept` with `const` + { a.method() } const -> std::same_as; +}; + +class NoExcept { +public: + // Note that we simply replaced `noexcept` with `const` + std::uint64_t method() const { + return 42; + } +}; + +void f(NoExceptMethod auto a) {} + +int main() { + NoExcept x{}; + + f(x); +} +``` + +```text +:6:19: error: expected ';' before 'const' + 6 | { a.method() } const -> std::same_as; + | ^~~~~~ + | ; +``` + +In general: exceptions add an orthogonal dimension of complexity on top of `const` because of how +difficult it is to deduce `noexcept` in practice. See also +http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1667r0.html + +Also, concepts getting so hard to understand that we write test cases: +https://andreasfertig.blog/2020/08/cpp20-concepts-testing-constrained-functions/ + +And for handling `volatile`: + +```c++ +#include +#include + +template +concept VolatileMethod = requires(volatile T a) { + { a.method() } -> std::same_as; +}; + +class Volatile { +public: + std::uint64_t method() volatile { + return 42; + } +}; + +void f(VolatileMethod auto a) { + a.method(); +} + +int main() { + Volatile x{}; + + f(x); +} +``` + +Though the compiler nicely warns us that we shouldn't do this: + +```text +:5:46: warning: 'volatile'-qualified parameter is deprecated [-Wvolatile] + 5 | concept VolatileMethod = requires(volatile T a) { + | ~~~~~~~~~~~^ +``` + +C++ also has `override`, but doesn't make much sense to impose that as a requirement; inheritance +and concepts are orthogonal systems. # Implement methods on remote types @@ -401,6 +566,13 @@ error message for `decltype()` is actually much nicer than the `static_assert`.. [type traits](http://en.cppreference.com/w/cpp/experimental/is_detected) to fix those issues, but mostly please just use concepts. +# Templated splatter + +Rust can't handle arbitrary numbers of template parameters. Can use macros, but I should investigate +`typename...` types. + +Common pattern to implement + # Potentially excluded Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth From 5e25faf5fd965ff8f743a8cb2cb140c2db9bbee5 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 03:03:15 -0400 Subject: [PATCH 16/25] I was using `const_cast<>` incorrectly --- _posts/2020-08-05-static-polymorphism.md | 52 ++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index f2ecb4e..b8dda6f 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -117,15 +117,15 @@ public: Rust allows declaring immutable or mutable. -C++ can use `const_cast` to assert "constness" of `this`: +Can require object we're calling methods on to be `const`: ```c++ #include #include template -concept ConstMethod = requires (T a) { - { const_cast(a).method() } -> std::same_as; +concept ConstMethod = requires (const T a) { + { a.method() } -> std::same_as; }; std::uint64_t my_function(ConstMethod auto a) { @@ -176,7 +176,51 @@ int main() { | ^~~~~~ ``` -...but can't mark `this` as consumed. +...which is equivalent to Rust's `&mut self`. Unlike Rust, can't mark `this` as consumed. + +Alternate form: using `static_cast<>` allows mixing some methods that are `const`, some not: + +```c++ +#include +#include + +template +concept ConstMethod = requires (T a) { + { static_cast(a).const_method() } -> std::same_as; + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; +}; + +std::uint64_t my_function(ConstMethod auto a) { + return a.method(); +} + +class HasConst { +public: + std::uint64_t const_method() const { + return 42; + } + + std::uint64_t nonconst_method() { + return 42; + } + + // Concept didn't require this to be `const`, but we can add the qualifier if we want. + std::uint64_t unnecessary_const_method() const { + return 42; + } +}; + +void f(ConstMethod auto x) {} + +int main() { + auto x = HasConst{}; + f(x); +} +``` + +May be better off defining `const T` methods in one concept, `T` methods in another, and then having +one concept that `requires` the sub-concepts, but just trying to demonstrate what is possible. Working with `const` parameters can be a bit weird because of implicit copies: From b2f7575ef5bbb59f001f58ca0bfded074b3add8a Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 10:49:33 -0400 Subject: [PATCH 17/25] Add some alternate forms for `const T a` --- _posts/2020-08-05-static-polymorphism.md | 42 ++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index b8dda6f..b563924 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -219,8 +219,46 @@ int main() { } ``` -May be better off defining `const T` methods in one concept, `T` methods in another, and then having -one concept that `requires` the sub-concepts, but just trying to demonstrate what is possible. +Alternate alternate form: + +```c++ +template +concept ConstMethod = requires { + requires requires (const T a) { + { a.const_method() } -> std::same_as; + }; + + requires requires (T a) { + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; + }; +}; +``` + +Third alternate form: + +```c++ +template +concept ConstMethods = requires (const T a) { + { a.const_method() } -> std::same_as; + }; + +template +concept NonConstMethods = requires (T a) { + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; + }; + + +template +concept ConstMethod = requires { + requires ConstMethods; + requires NonConstMethods; +}; +``` + +...which goes a long way towards explaining why the "requires requires" form is necessary. Not sure +what the "best practices" form is, just trying to demonstrate what is possible. Working with `const` parameters can be a bit weird because of implicit copies: From a939ceaba74d309bf1170f6499525e6c13829d42 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 13:41:39 -0400 Subject: [PATCH 18/25] So many different ways to express the same thing. --- _posts/2020-08-05-static-polymorphism.md | 28 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index b563924..f4ee2dc 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -223,6 +223,18 @@ Alternate alternate form: ```c++ template +concept ConstMethod = + requires (const T a) { + { a.const_method() } -> std::same_as; + } && + requires (T a) { + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; + }; + +// Formulated inside a `requires` block: +/* +template concept ConstMethod = requires { requires requires (const T a) { { a.const_method() } -> std::same_as; @@ -233,6 +245,7 @@ concept ConstMethod = requires { { a.unnecessary_const_method() } -> std::same_as; }; }; +*/ ``` Third alternate form: @@ -240,21 +253,26 @@ Third alternate form: ```c++ template concept ConstMethods = requires (const T a) { - { a.const_method() } -> std::same_as; - }; + { a.const_method() } -> std::same_as; +}; template concept NonConstMethods = requires (T a) { - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; - }; + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; +}; +template +concept ConstMethod = ConstMethods && NonConstMethods; +// Formulated inside a requires block: +/* template concept ConstMethod = requires { requires ConstMethods; requires NonConstMethods; }; +*/ ``` ...which goes a long way towards explaining why the "requires requires" form is necessary. Not sure From 7fa111a68a2f9b564c75f31d6c4bb41923371e1e Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 13:57:07 -0400 Subject: [PATCH 19/25] Yet more alternative styles --- _posts/2020-08-05-static-polymorphism.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index f4ee2dc..557b340 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -232,6 +232,20 @@ concept ConstMethod = { a.unnecessary_const_method() } -> std::same_as; }; +// Can also use parentheses: +/* +template +concept ConstMethod = ( + requires (const T a) { + { a.const_method() } -> std::same_as; + } && + requires (T a) { + { a.nonconst_method() } -> std::same_as; + { a.unnecessary_const_method() } -> std::same_as; + } +); +*/ + // Formulated inside a `requires` block: /* template From 28d28b97aa3d70d9bba7c3d084b8f2d0e96a4624 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 30 Aug 2020 23:48:27 -0400 Subject: [PATCH 20/25] Minor note about `final` --- _posts/2020-08-05-static-polymorphism.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 557b340..74e7225 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -536,8 +536,8 @@ Though the compiler nicely warns us that we shouldn't do this: | ~~~~~~~~~~~^ ``` -C++ also has `override`, but doesn't make much sense to impose that as a requirement; inheritance -and concepts are orthogonal systems. +C++ also has `override` and `final`, but doesn't make much sense to impose that as a requirement; +inheritance and concepts are orthogonal systems. # Implement methods on remote types From 5ba3978f2cd3e5972c7f584e8a0a282dde6ca306 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Thu, 3 Sep 2020 00:58:45 -0400 Subject: [PATCH 21/25] Notes on splatter/varargs and CRTP --- _posts/2020-08-05-static-polymorphism.md | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 74e7225..058192a 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -682,16 +682,37 @@ mostly please just use concepts. # Templated splatter -Rust can't handle arbitrary numbers of template parameters. Can use macros, but I should investigate -`typename...` types. +Rust can't handle arbitrary numbers of template parameters. Macros can (see `vec![]`), and you could +use a macro to define the implementation of free functions, but methods need to know exactly what +the arguments are. Also, no concept of SFINAE or type inspection in Rust macros. -Common pattern to implement +Good example of how to demonstrate Rust not being able to use splatter templates: can't emplace back +on a vector. Have to construct and move. In general, don't think it's possible to "construct" into a +pre-existing address; could use same variation of unsafe to say "I know how large i am, I know my +layout, I have a pointer to where I begin, set it all up," but even that would have to be defined on +the struct, `Vec` can't forward args to this initializer method. +That said, open question as to whether the move vs. construct-in-place/placement new matters given +an optimizing compiler: https://stackoverflow.com/a/36919571 # Potentially excluded Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth investigating in a future post? +# CRTP + +Might not need to be an extensive section? CRTP lets bases reference children. Rust has no struct +inheritance, but some CRTP stuff can be used with traits. + +Review of the examples Wikipedia gives: + +- Static polymorphism: Traits are allowed to declare that implementors define an `implementation()` + and then provide default implementations of other methods that use it (without virtual calls). Not + a common pattern though; use composition, not inheritance. https://godbolt.org/z/Md55e7 +- Object counter: I don't think Rust has a way to accomplish this; traits aren't allowed to hold + data. +- Polymorphic chaining: Feel free to return `Self`, `&Self`, etc., builder patterns aren't new. + ## Visibility Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that From 3e4760a7278988bb7ff06106e3378d54b31bf4da Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Mon, 7 Sep 2020 02:20:23 -0400 Subject: [PATCH 22/25] std::initializer_list --- _posts/2020-08-05-static-polymorphism.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 058192a..2d49080 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -694,10 +694,7 @@ the struct, `Vec` can't forward args to this initializer method. That said, open question as to whether the move vs. construct-in-place/placement new matters given an optimizing compiler: https://stackoverflow.com/a/36919571 -# Potentially excluded - -Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth -investigating in a future post? +Also: `std::initializer_list` (although Rust can get _very_ close with macros: `vec!`). # CRTP @@ -713,6 +710,11 @@ Review of the examples Wikipedia gives: data. - Polymorphic chaining: Feel free to return `Self`, `&Self`, etc., builder patterns aren't new. +# Potentially excluded + +Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth +investigating in a future post? + ## Visibility Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that From fd04a5131987947c27621cbaca536825b5e7cf46 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Tue, 8 Sep 2020 00:28:56 -0400 Subject: [PATCH 23/25] Initializer list doesn't use template splatter --- _posts/2020-08-05-static-polymorphism.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 2d49080..54ee9cf 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -694,8 +694,6 @@ the struct, `Vec` can't forward args to this initializer method. That said, open question as to whether the move vs. construct-in-place/placement new matters given an optimizing compiler: https://stackoverflow.com/a/36919571 -Also: `std::initializer_list` (although Rust can get _very_ close with macros: `vec!`). - # CRTP Might not need to be an extensive section? CRTP lets bases reference children. Rust has no struct From c27b6b9dd1096fb78bca7ba0daecf4bae0eb1eca Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Thu, 10 Sep 2020 16:26:54 -0400 Subject: [PATCH 24/25] CPPInsights thinks concepts aren't reducible to templates --- _posts/2020-08-05-static-polymorphism.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md index 54ee9cf..3cb670d 100644 --- a/_posts/2020-08-05-static-polymorphism.md +++ b/_posts/2020-08-05-static-polymorphism.md @@ -12,7 +12,10 @@ useful because both languages are "system." # System Differences Worth noting differences in goals: polymorphism in C++ is only duck typing. Means that static -polymorphism happens separate from visibility, overloading, etc. +polymorphism happens separate from visibility, overloading, etc. Concepts appear to be more than an +extension of template metaprogramming +([example](https://cppinsights.io/lnk?code=I2luY2x1ZGUgPGNvbmNlcHRzPgojaW5jbHVkZSA8Y3N0ZGludD4KCnRlbXBsYXRlPHR5cGVuYW1lIFQ+CmNvbmNlcHQgSGFzTWV0aG9kID0gcmVxdWlyZXMgKFQgYSkgewogIHsgYS5tZXRob2QoKSB9IC0+IHN0ZDo6c2FtZV9hczxzdGQ6OnVpbnQ2NF90PjsKfTsKCmNsYXNzIE15Q2xhc3MgewpwdWJsaWM6CiAgc3RkOjp1aW50NjRfdCBtZXRob2QoKSB7CiAgICByZXR1cm4gMDsKICB9Cn07Cgp2b2lkIGZ1bmN0aW9uKEhhc01ldGhvZCBhdXRvIGEpIHsKICBhLm1ldGhvZCgpOwp9CgppbnQgbWFpbigpCnsKICBNeUNsYXNzIHh7fTsKICAKICBmdW5jdGlvbih4KTsKfQ==&insightsOptions=cpp2a&std=cpp2a&rev=1.0)), +but don't enable anything that wasn't possible otherwise (was just a monstrous pain previously). Rust's trait system is more thorough (need a better way to explain that), which allows for trait markers, auto-deriving, arbitrary self. From cd5066953c78ad1d95d09d3f4e391b214ba80cf5 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 10 Nov 2024 16:34:27 -0500 Subject: [PATCH 25/25] Revert --- _posts/2018-09-15-isomorphic-apps.md | 2 +- _posts/2020-08-05-static-polymorphism.md | 892 ----------------------- 2 files changed, 1 insertion(+), 893 deletions(-) delete mode 100644 _posts/2020-08-05-static-polymorphism.md diff --git a/_posts/2018-09-15-isomorphic-apps.md b/_posts/2018-09-15-isomorphic-apps.md index 00a926c..abc0dcb 100644 --- a/_posts/2018-09-15-isomorphic-apps.md +++ b/_posts/2018-09-15-isomorphic-apps.md @@ -98,7 +98,7 @@ The first issue [I ran into](https://www.reddit.com/r/rust/comments/98lpun/unable_to_load_wasm_for_electron_application/) while attempting to bundle everything via `webpack` is a detail in the WASM spec: -> This function accepts a Response object, or a promise for one, and ... **[if it] does not match +> This function accepts a Response object, or a promise for one, and ... **[if > it] does not match > the `application/wasm` MIME type**, the returned promise will be rejected with a TypeError; > > [WebAssembly - Additional Web Embedding API](https://webassembly.org/docs/web/#additional-web-embedding-api) diff --git a/_posts/2020-08-05-static-polymorphism.md b/_posts/2020-08-05-static-polymorphism.md deleted file mode 100644 index 3cb670d..0000000 --- a/_posts/2020-08-05-static-polymorphism.md +++ /dev/null @@ -1,892 +0,0 @@ ---- -layout: post -title: "Static Polymorphism" -description: "Emulating Traits in C++" -category: -tags: [python] ---- - -Other languages have done similar things (interfaces in Java), but think the Rust comparison is -useful because both languages are "system." - -# System Differences - -Worth noting differences in goals: polymorphism in C++ is only duck typing. Means that static -polymorphism happens separate from visibility, overloading, etc. Concepts appear to be more than an -extension of template metaprogramming -([example](https://cppinsights.io/lnk?code=I2luY2x1ZGUgPGNvbmNlcHRzPgojaW5jbHVkZSA8Y3N0ZGludD4KCnRlbXBsYXRlPHR5cGVuYW1lIFQ+CmNvbmNlcHQgSGFzTWV0aG9kID0gcmVxdWlyZXMgKFQgYSkgewogIHsgYS5tZXRob2QoKSB9IC0+IHN0ZDo6c2FtZV9hczxzdGQ6OnVpbnQ2NF90PjsKfTsKCmNsYXNzIE15Q2xhc3MgewpwdWJsaWM6CiAgc3RkOjp1aW50NjRfdCBtZXRob2QoKSB7CiAgICByZXR1cm4gMDsKICB9Cn07Cgp2b2lkIGZ1bmN0aW9uKEhhc01ldGhvZCBhdXRvIGEpIHsKICBhLm1ldGhvZCgpOwp9CgppbnQgbWFpbigpCnsKICBNeUNsYXNzIHh7fTsKICAKICBmdW5jdGlvbih4KTsKfQ==&insightsOptions=cpp2a&std=cpp2a&rev=1.0)), -but don't enable anything that wasn't possible otherwise (was just a monstrous pain previously). - -Rust's trait system is more thorough (need a better way to explain that), which allows for trait -markers, auto-deriving, arbitrary self. - -# Simple Example - -Accept parameter types, return known type. Also needs to be generic over parameter types. - -Should make a quick note that C++ doesn't allow - -# Generic return - -Same name and parameter signature, but return different types - `AsRef` - -# Associated types - -`.as_iter()`, and the iterator `Item` type - -# decltype and compiler-named types - -Rust has some types named by the compiler, but inaccessible in traits; can't return `impl SomeTrait` -from traits. Can return `impl Future` from free functions and structs, but traits can't use -compiler-generated types (associated types still need to name the type). - -Can have traits return references (`&dyn Trait`), but uses vtable (so no longer statically -polymorphic), and very likely get into all sorts of lifetime issues. Can use `Box` trait -objects to avoid lifetime issues, but again, uses vtable. - -C++ doesn't appear to have the same restrictions, mostly because the "contract" is just duck typing. - -# Require static methods on a class? - -Shouldn't be too hard - `T::some_method()` should be compilable. - -# Default implementation - -First: example of same name, different arguments. Not possible in Rust. - -```rust -trait MyTrait { - // This is illegal in Rust, even though name-mangling is unique: - // fn method(&self, value: usize) -> usize; - - // Works if you rename the method, but is a pain to type: - fn method_with_options(&self, value: usize) -> usize; - fn method(&self) -> usize { - self.method_with_options(42); - } -} - -struct MyStruct {} -impl MyTrait for MyStruct { - fn method_with_options(&self, value: usize) -> usize { - println!("{}", value); - value - } -} -``` - -Second: example of same name, different arguments, but can't provide default implementation. - -```c++ -template -concept MyTrait = requires (T a) { - { a.method(declval()) } -> std::same_as, - { a.method() } -> std::same_as, -} - -// Each class must implement both `method` signatures. -class MyClass { -public: - std::size_t method(std::size_t value) { - std::cout << value << std::endl; - return value; - } - - std::size_t method() { - return method(42); - } -}; - -// Can write free functions as the default and then call explicitly, but for trivial -// implementations (replacing defaults) it's not likely to be worth it. -auto method_default_(auto MyTrait this) std::size_t { - return this.method(42); -} - -class MyClassDefault { -public: - std::size_t method(std::size_t value) { - std::cout << value << std::endl; - return value; - } - - std::size_t method() { - return method_default_(this); - } -} -``` - -# Method Qualifiers - -Rust allows declaring immutable or mutable. - -Can require object we're calling methods on to be `const`: - -```c++ -#include -#include - -template -concept ConstMethod = requires (const T a) { - { a.method() } -> std::same_as; -}; - -std::uint64_t my_function(ConstMethod auto a) { - return a.method(); -} - -class HasConst { -public: - std::uint64_t method() const { - return 42; - } -}; - -class WithoutConst { -public: - std::uint64_t method() { - return 42; - } -}; - -int main() { - auto x = HasConst{}; - my_function(x); - - auto y = WithoutConst{}; - my_function(y); -} -``` - -```text -:32:18: error: use of function 'uint64_t my_function(auto:1) [with auto:1 = WithoutConst; uint64_t = long unsigned int]' with unsatisfied constraints - 32 | my_function(y); - | ^ -:9:15: note: declared here - 9 | std::uint64_t my_function(ConstMethod auto a) { - | ^~~~~~~~~~~ -:9:15: note: constraints not satisfied -: In instantiation of 'uint64_t my_function(auto:1) [with auto:1 = WithoutConst; uint64_t = long unsigned int]': -:32:18: required from here -:5:9: required for the satisfaction of 'ConstMethod' [with auto:1 = WithoutConst] -:5:23: in requirements with 'T a' [with T = WithoutConst] -:6:37: note: the required expression 'const_cast(a).method()' is invalid, because - 6 | { const_cast(a).method() } -> std::same_as; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~ -:6:37: error: passing 'const WithoutConst' as 'this' argument discards qualifiers [-fpermissive] -:22:19: note: in call to 'uint64_t WithoutConst::method()' - 22 | std::uint64_t method() { - | ^~~~~~ -``` - -...which is equivalent to Rust's `&mut self`. Unlike Rust, can't mark `this` as consumed. - -Alternate form: using `static_cast<>` allows mixing some methods that are `const`, some not: - -```c++ -#include -#include - -template -concept ConstMethod = requires (T a) { - { static_cast(a).const_method() } -> std::same_as; - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; -}; - -std::uint64_t my_function(ConstMethod auto a) { - return a.method(); -} - -class HasConst { -public: - std::uint64_t const_method() const { - return 42; - } - - std::uint64_t nonconst_method() { - return 42; - } - - // Concept didn't require this to be `const`, but we can add the qualifier if we want. - std::uint64_t unnecessary_const_method() const { - return 42; - } -}; - -void f(ConstMethod auto x) {} - -int main() { - auto x = HasConst{}; - f(x); -} -``` - -Alternate alternate form: - -```c++ -template -concept ConstMethod = - requires (const T a) { - { a.const_method() } -> std::same_as; - } && - requires (T a) { - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; - }; - -// Can also use parentheses: -/* -template -concept ConstMethod = ( - requires (const T a) { - { a.const_method() } -> std::same_as; - } && - requires (T a) { - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; - } -); -*/ - -// Formulated inside a `requires` block: -/* -template -concept ConstMethod = requires { - requires requires (const T a) { - { a.const_method() } -> std::same_as; - }; - - requires requires (T a) { - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; - }; -}; -*/ -``` - -Third alternate form: - -```c++ -template -concept ConstMethods = requires (const T a) { - { a.const_method() } -> std::same_as; -}; - -template -concept NonConstMethods = requires (T a) { - { a.nonconst_method() } -> std::same_as; - { a.unnecessary_const_method() } -> std::same_as; -}; - -template -concept ConstMethod = ConstMethods && NonConstMethods; - -// Formulated inside a requires block: -/* -template -concept ConstMethod = requires { - requires ConstMethods; - requires NonConstMethods; -}; -*/ -``` - -...which goes a long way towards explaining why the "requires requires" form is necessary. Not sure -what the "best practices" form is, just trying to demonstrate what is possible. - -Working with `const` parameters can be a bit weird because of implicit copies: - -```c++ -#include -#include - -class WithCopyCtor { -public: - WithCopyCtor(const WithCopyCtor &other) = default; -}; - -class WithoutCopyCtor { -public: - WithoutCopyCtor(const WithoutCopyCtor &other) = delete; -}; - -template -concept ConstArgument = requires (T a) { - // Arguments passed by value: - { a.method_one(std::declval()) } -> std::same_as; - { a.method_two(std::declval()) } -> std::same_as; - - // Arguments passed by reference: - { a.method_three(std::declval()) } -> std::same_as; - { a.method_four(std::declval()) } -> std::same_as; - - // NOTE: This requirement is illogical. It's impossible to call a method accepting a parameter - // by value when that parameter can not copy construct. - // Not sure if it's worth including this note in the final write-up though. - //{ a.method_four(std::declval()) } -> std::same_as; - - { a.method_five(std::declval()) } -> std::same_as; -}; - -std::uint64_t my_function(ConstArgument auto a) { - return 42; -} - -class MyClass { -public: - // NOTE: Even though the concept required `method_one` to accept `const std::uint64_t`, we don't need - // to use a `const` qualifier here because we can implicitly copy `const std::uint64_t` to `std::uint64_t`. - std::uint64_t method_one(std::uint64_t value) { - return 42; - } - - // NOTE: Similar to `method_one`, even though the concept declared `const WithCopyCtor`, - // we can use the copy constructor to implicitly copy and convert between `const` and non-`const`. - std::uint64_t method_two(WithCopyCtor value) { - return 42; - } - - // NOTE: Because we can't implicitly copy from `const` references to non-`const` references, - // _even if the class has a copy constructor_, we must include the qualifier here. - std::uint64_t method_three(const WithCopyCtor &value) { - return 42; - } - - // NOTE: Similar to `method_three`, because we can't copy from `const` rvalue references to non-`const`, - // we must include the qualifier. - std::uint64_t method_four(const WithoutCopyCtor &&value) { - return 42; - } - - // NOTE: We can _add_ a `const` qualifier even if the concept doesn't require it, because it's safe to - // treat non-`const` references as `const. - std::uint64_t method_five(const WithoutCopyCtor &value) { - return 42; - } -}; - -int main() { - auto x = MyClass{}; - my_function(x); -} -``` - -Rust is much simpler about all this - the signature for a trait implementation must _exactly_ match -a trait definition. Actual usage rules may be weird (what happens with a mut reference -`#[derive(Copy)]` struct when a function takes immutable by value?), but the polymorphic side stays -consistent. - -Can also use `noexcept` qualifier. Not sure why this has issues: - -```c++ -#include -#include - -template -concept NoExceptMethod = requires (T a) { - { noexcept(a.method()) } -> std::same_as; -}; - -class NoExcept { -public: - std::uint64_t method() { - return 42; - } -}; - -void f(NoExceptMethod auto a) {} - -int main() { - NoExcept x{}; - - f(x); -} -``` - -Or why this is allowable: - -```c++ -#include -#include - -template -concept NoExceptMethod = requires (T a) { - { a.method() } -> std::same_as; - noexcept(a.method()); -}; - -class NoExcept { -public: - std::uint64_t method() { - return 42; - } -}; - -void f(NoExceptMethod auto a) {} - -int main() { - NoExcept x{}; - - f(x); -} -``` - -Turns out this is the way to do it: - -```c++ -#include -#include - -template -concept NoExceptMethod = requires (T a) { - { a.method() } noexcept -> std::same_as; -}; - -class NoExcept { -public: - std::uint64_t method() noexcept { - return 42; - } -}; - -void f(NoExceptMethod auto a) {} - -int main() { - NoExcept x{}; - - f(x); -} -``` - -But this doesn't compile? - -```c++ -#include -#include - -template -concept NoExceptMethod = requires (T a) { - // Note that we simply replaced `noexcept` with `const` - { a.method() } const -> std::same_as; -}; - -class NoExcept { -public: - // Note that we simply replaced `noexcept` with `const` - std::uint64_t method() const { - return 42; - } -}; - -void f(NoExceptMethod auto a) {} - -int main() { - NoExcept x{}; - - f(x); -} -``` - -```text -:6:19: error: expected ';' before 'const' - 6 | { a.method() } const -> std::same_as; - | ^~~~~~ - | ; -``` - -In general: exceptions add an orthogonal dimension of complexity on top of `const` because of how -difficult it is to deduce `noexcept` in practice. See also -http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1667r0.html - -Also, concepts getting so hard to understand that we write test cases: -https://andreasfertig.blog/2020/08/cpp20-concepts-testing-constrained-functions/ - -And for handling `volatile`: - -```c++ -#include -#include - -template -concept VolatileMethod = requires(volatile T a) { - { a.method() } -> std::same_as; -}; - -class Volatile { -public: - std::uint64_t method() volatile { - return 42; - } -}; - -void f(VolatileMethod auto a) { - a.method(); -} - -int main() { - Volatile x{}; - - f(x); -} -``` - -Though the compiler nicely warns us that we shouldn't do this: - -```text -:5:46: warning: 'volatile'-qualified parameter is deprecated [-Wvolatile] - 5 | concept VolatileMethod = requires(volatile T a) { - | ~~~~~~~~~~~^ -``` - -C++ also has `override` and `final`, but doesn't make much sense to impose that as a requirement; -inheritance and concepts are orthogonal systems. - -# Implement methods on remote types - -Rust allows both arbitrary `self` and extension traits. Arbitrary self forms the basis of the -`async` system in Rust. Extension traits form basis of `futures` library. Accomplish effectively the -same thing, but for concrete types and traits respectively. - -UFCS would achieve the same effect, but unclear if/when it will be available: -https://dancrn.com/2020/08/02/ufcs-in-clang.html - -Can use free functions in the meantime, but having the IDE auto-complete `.` is -exceedingly useful, as opposed to looking through all functions in a namespace. - -Can also sub-class or implicitly convert to a wrapper: - -```c++ -#include -#include - -class SomeRemoteClass {}; - -template -concept MyConcept = requires (T a) { - { a.do_something() } -> std::same_as; -}; - -// Note: It's unsafe to move `SomeRemoteClass`, so we accept by reference -// Requiring SomeRemoteClass be copy-constructible would also be OK. -class LocalImpl { -public: - LocalImpl(const SomeRemoteClass &remote): remote_{remote} {}; - std::uint64_t do_something() { - return 42; - } - -private: - const SomeRemoteClass &remote_; -}; - -auto auto_func(MyConcept auto value) { - auto x = value.do_something(); -} - -void regular_func(LocalImpl value) { - auto x = value.do_something(); -} - -int main() { - SomeRemoteClass x {}; - - // This _will not_ compile because `auto` doesn't trigger the conversion to `LocalImpl` - //auto_func(x); - - // This _will_ compile because the function signature declares a concrete class for which an - // implicit conversion is available. It just so happens that `LocalImpl` satisfies `MyConcept`. - regular_func(x); -} -``` - -The `LocalImpl` wrapper could be extended to handle additional remote types using template -specialization or holding an internal `std::variant`, but that misses the point: we want to write -code that accepts anything that satisfies `MyConcept`. When we write functions that require a -specific wrapper, we're being overly restrictive, and obfuscating our intentions (we don't actually -care about the wrapper, it's just there for ease-of-use). - -Can use some overloading/specialization tricks for ease of use: - -```c++ -auto some_func_(MyConcept auto value) -> void { - auto x = value.do_something(); -} - -auto some_func(MyConcept auto value) -> void { - some_func_(value); -} - -void some_func(LocalImpl value) { - some_func_(value); -} -``` - -Need to be careful though: - -```c++ -auto some_func(MyConcept auto value) -> void { - auto x = value.do_something(); -} - -void some_func(LocalImpl value) { - // NOTE: Because `LocalImpl` is more specific than `auto`, this is a recursive call and - // will overflow the stack. - // We use `some_func_` above to uniquely name the function we actually want to call. - some_func(value); -} -``` - -Potentially worth mentioning orphan rule in Rust as limit to extension methods - can't implement -remote traits for remote types. - -# Checking a type fulfills the concept - -With concepts, you find out that there's an issue only when you attempt to use it. Traits in Rust -will let you know during implementation that something is wrong (there's a local error). -https://www.ecorax.net/as-above-so-below-1/ - -Can use `static_assert` to kinda make sure a contract is fulfilled: - -```c++ -#include -#include - -template -constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; - -class WithMethod { -public: - std::uint64_t method() { return 0; } -}; - -static_assert(has_method); - -class WithoutMethod {}; - -// : In instantiation of 'constexpr const bool has_method': -// :16:16: required from here -// :5:71: error: 'class WithoutMethod' has no member named 'method' -// 5 | constexpr bool has_method = std::is_same_v().method()), std::uint64_t>; -// | ~~~~~~~~~~~~~~~~~~^~~~~~ -// :16:15: error: non-constant condition for static assertion -// 16 | static_assert(has_method); -// | -static_assert(has_method); -``` - -We'd rather the example fail the static assert, rather than have an error on the `decltype`, but it -does get the job done; we're told explicitly that `WithoutMethod` has no member `method`, so the -error message for `decltype()` is actually much nicer than the `static_assert`.. Can use -[custom SFINAE](https://stackoverflow.com/a/257382) or -[experimental](https://stackoverflow.com/a/22014784) -[type traits](http://en.cppreference.com/w/cpp/experimental/is_detected) to fix those issues, but -mostly please just use concepts. - -# Templated splatter - -Rust can't handle arbitrary numbers of template parameters. Macros can (see `vec![]`), and you could -use a macro to define the implementation of free functions, but methods need to know exactly what -the arguments are. Also, no concept of SFINAE or type inspection in Rust macros. - -Good example of how to demonstrate Rust not being able to use splatter templates: can't emplace back -on a vector. Have to construct and move. In general, don't think it's possible to "construct" into a -pre-existing address; could use same variation of unsafe to say "I know how large i am, I know my -layout, I have a pointer to where I begin, set it all up," but even that would have to be defined on -the struct, `Vec` can't forward args to this initializer method. -That said, open question as to whether the move vs. construct-in-place/placement new matters given -an optimizing compiler: https://stackoverflow.com/a/36919571 - -# CRTP - -Might not need to be an extensive section? CRTP lets bases reference children. Rust has no struct -inheritance, but some CRTP stuff can be used with traits. - -Review of the examples Wikipedia gives: - -- Static polymorphism: Traits are allowed to declare that implementors define an `implementation()` - and then provide default implementations of other methods that use it (without virtual calls). Not - a common pattern though; use composition, not inheritance. https://godbolt.org/z/Md55e7 -- Object counter: I don't think Rust has a way to accomplish this; traits aren't allowed to hold - data. -- Polymorphic chaining: Feel free to return `Self`, `&Self`, etc., builder patterns aren't new. - -# Potentially excluded - -Some ideas related to traits, but that I'm not sure sufficiently fit the theme. May be worth -investigating in a future post? - -## Visibility - -Worth acknowledging that C++ can do interesting things with `protected`, `friend`, and others, that -Rust can't. However, Rust can limit trait implementations to current crate ("sealed traits"), where -C++ concepts are purely duck typing. - -## Move/consume `self` as opposed to `&self`? - -Handled as part of method qualifiers. - -Not exactly polymorphism, but is a significant feature of Rust trait system. Is there a way to force -`std::move(object).method()`? C++ can still use objects after movement makes them invalid, so not -sure that it makes conceptual sense - it's your job to prevent use-after-move, not the compiler's. - -## Automatic markers? - -Alternately, conditional inheritance based on templates? - -## Arbitrary `self` - -Handled as part of section on `impl Trait` for remote type, not sure this needs it's own section. - -Forms the basis for Rust's async system, but used very rarely aside from that. - -[`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 and function similar to C, 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. - -That said, it is interesting that for Rust, arbitrary self can be replaced with traits: - -```rust -trait MyTrait { - fn my_function(&self); -} - -impl MyTrait for Box { - fn my_function(&self) {} -} -``` - -Just have to make sure that `MyTrait` is in scope all the time, and that's not fun. Ultimately, Rust -kinda already has UFCS. It's only "kinda" because you have to bring it in scope, and it's -potentially unclear when it's being used (extension traits), but it does get the basic job done. - -# Trait objects as arguments - -Handled as part of `decltype` and compiler-named types, not sure it needs it's own section. - -```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) {} -``` - -Alternate form with concepts: - -```c++ -#include -#include - -template -concept HasMethod = requires (T a) { - { a.some_method() } -> std::same_as; -}; - -auto my_function(HasMethod auto value) { - auto x = value.some_method(); -} - -class MyClass { -public: - std::uint64_t some_method() { - return 42; - } -}; - -int main() { - auto x = MyClass {}; - my_function(x); -} -``` - -vtable is automatically used if virtual, but concepts (so far as I can tell) can't detect virtual. - -Kind of nice because you don't have to explicitly manage the vtable in Rust, but you trade off the -ability to get inheritance. Modern trends have been "composition over inheritance" (see Google style -docs as an example) so the trend may be worth it, but moving away from inheritance models is -disorienting. - -`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.