5.2 KiB
layout | title | description | category | tags | |
---|---|---|---|---|---|
post | Static Polymorphism | Emulating Traits in C++ |
|
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
enable_unique_from_this
doesn't make a whole lot of sense, but Rust can do it:
struct MyStruct {}
impl MyStruct {
fn my_function(self: &Box<Self>) {}
}
fn main() {
let unboxed = MyStruct {};
// error[E0599]: no method named `my_function` found for struct `MyStruct` in the current scope
// unboxed.my_function();
let boxed = Box::new(MyStruct {});
boxed.my_function();
boxed.my_function();
}
Interestingly enough, can't bind static
version using equality:
#include <iterator>
#include <vector>
#include <concepts>
std::uint64_t free_get_value() {
return 24;
}
class MyClass {
public:
// <source>:11:47: error: invalid pure specifier (only '= 0' is allowed) before ';' token
std::uint64_t get_value() = free_get_value;
};
int main() {
auto x = MyClass {};
}
Turns out the purpose of enable_shared_from_this
is so that you can create new shared instances of
yourself from within yourself, it doesn't have anything to do with enabling extra functionality
depending on whether you're owned by a shared pointer. At best, you could have other runtime
checks to see if you're owned exclusively, or as part of some other smart pointer, but the type
system can't enforce that. And if you're not owned by that smart pointer, what then? Exceptions?
UFCS would be able to help with this - define new methods like:
template<>
void do_a_thing(std::unique_ptr<MyType> value) {}
In this case, the extension is actually on unique_ptr
, but the overload resolution applies only to
pointers of MyType
. Note that shared_ptr
and others seem to work by overloading operator ->
to
proxy function calls to the delegates; you could inherit std::shared_ptr
and specialize the
template to add methods for specific classes I guess? But it's still inheriting shared_ptr
, you
can't define things directly on it.
Generally, "you can just use free functions" seems like a shoddy explanation. We could standardize
overload MyClass_init
as a constructor, etc., but the language is designed to assist us so we
don't have to do crap like that. I do hope UFCS becomes a thing.
Default implementation
First: example of same name, different arguments. Not possible in Rust.
Can you bind a free function in a non-static way? Pseudocode:
template<typename T>
concept DoMethod = requires (T a) {
{ a.do_method(std::declval<std::uint64_t>() } -> std::same_as<std::uint64_t>;
{ a.do_method() } -> std::same_as<std::uint64_t>;
}
template<typename T> requires DoMethod<T>
std::uint64_t free_do_method(T& a) {
a.do_method(0);
}
class MyClass {
public:
std::uint64_t do_method(std::uint64_t value) {
return value * 2;
}
// Because the free function still needs a "this" reference (unlike Javascript which has a
// floating `this`), we can't bind as `std::uint64_t do_method() = free_do_method`
std::uint64_t do_method() {
return free_do_method(this);
}
};
Require concept methods to take const this
?
Move/consume self
as opposed to &self
?
Is there a way to force std::move(object).method()
?
Require static methods on a class?
override
, or other means of verifying a function implements a requirement?
Local trait implementation of remote types?
AKA "extension methods". UFCS can accomplish this, and could use free functions to handle instead,
but having the IDE auto-complete .<the next thing>
is exceedingly useful, as opposed to memorizing
what functions are necessary for conversion. We're not changing what's possible, just making it
easier for humans.
Likely requires sub-classing the remote class. Implicit conversions don't really work because they must be defined on the remote type.
Rust makes this weird because you have to use ClientExt
to bring the methods in scope, but the
trait name might not show up because impl ClientExt for RemoteStruct
is defined elsewhere.
Alternately, ClientExt: AnotherTrait
implementations where the default ClientExt
implementation
is used. 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
trait MyTrait {
fn some_method(&self);
}
fn my_function(value: &dyn MyTrait) {
}
C++ can't explicitly use vtable as part of concepts:
template<typename T, typename = std::enable_if_t<...>>
void my_function(T& value) {}
...is equivalent to:
fn my_function<T: MyTrait>(value: &T) {}
vtable is automatically used if declared virtual.
dyn Trait
seems to be used in Rust mostly for type erasure - Box<Pin<dyn Future>>
for example,
but is generally fairly rare, and C++ probably doesn't suffer for not having it. Can use inheritance
to force virtual if truly necessary, but not sure why you'd need that.