Trait objects and helper methods
There is a pattern in C++ and other languages where you create an interface with some methods to be overridden and some final helper methods (typically inlined) for convenience.
I’ll use the following example (cpp.sh):
#include <string>
#include <typeinfo>
#include <cstring>
#include <cassert>
class IQuery {
public:
virtual const void* Query(const std::type_info& ty, const char* name) const = 0;
template<typename T>
inline const T* Get(const char* name) const {
return (const T*)this->Query(typeid(T), name);
}
};
class Parameters : public IQuery {
public:
Parameters(int i, float f, std::string s) : i(i), f(f), s(std::move(s)) {}
virtual const void* Query(const std::type_info& ty, const char* name) const {
if (!strcmp(name, "i")) {
return ty == typeid(int) ? &this->i : nullptr;
}
else if (!strcmp(name, "f")) {
return ty == typeid(float) ? &this->f : nullptr;
}
else if (!strcmp(name, "s")) {
return ty == typeid(std::string) ? &this->s : nullptr;
}
return nullptr;
}
private:
int i;
float f;
std::string s;
};
int main()
{
Parameters p(42, 3.1415927, "Hello world!");
assert(*p.Get<int>("i") == 42);
assert(*p.Get<float>("f") == 3.1415927);
assert(*p.Get<std::string>("s") == "Hello world!");
IQuery* q = &p;
assert(*q->Get<int>("i") == 42);
assert(*q->Get<float>("f") == 3.1415927);
assert(*q->Get<std::string>("s") == "Hello world!");
}
The example demonstrates the pattern of creating an implementable interface with convenience methods to handle the ugly casting.
A first attempt to convert this to Rust: playground
use ::std::any::{Any};
trait IQuery {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any>;
fn get<'s, T: 'static>(&'s self, name: &str) -> Option<&'s T> {
self.query(name).and_then(|val| val.downcast_ref())
}
}
struct Parameters {
i: i32,
f: f64,
s: String,
}
impl IQuery for Parameters {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any> {
match name {
"i" => Some(&self.i),
"f" => Some(&self.f),
"s" => Some(&self.s),
_ => None,
}
}
}
fn main() {
let p = Parameters {
i: 42,
f: 3.1415927,
s: String::from("Hello world!"),
};
assert_eq!(p.get("i"), Some(&42));
assert_eq!(p.get("f"), Some(&3.1415927));
assert_eq!(p.get("s"), Some(&String::from("Hello world!")));
let q = &p as &IQuery;
assert_eq!(q.get("i"), Some(&42));
assert_eq!(q.get("f"), Some(&3.141592));
assert_eq!(q.get("s"), Some(&String::from("Hello world!")));
}
This works but has an unfortunate limitation: the trait isn’t object-safe!
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0038]: the trait `IQuery` cannot be made into an object
--> <anon>:37:16
|
37 | let q = &p as &IQuery;
| ^^^^^^^ the trait `IQuery` cannot be made into an object
|
= note: method `get` has generic type parameters
Rust offers a canonical solution [1]: mark the offending methods with where Self: Sized
bounds: playground
trait IQuery {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any>;
fn get<'s, T: 'static>(&'s self, name: &str) -> Option<&'s T>
where Self: Sized
{
self.query(name).and_then(|val| val.downcast_ref())
}
}
Unfortunately this won’t get us the whole way there, see we’ve filtered out the convenience methods such that they’re completely unavailable on trait objects!
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0277]: the trait bound `IQuery: std::marker::Sized` is not satisfied
--> <anon>:40:15
|
40 | assert_eq!(q.get("i"), Some(&42));
| ^^^ the trait `std::marker::Sized` is not implemented for `IQuery`
|
= note: `IQuery` does not have a constant size known at compile-time
This somewhat confusing error message is trying to say you cannot call get
on a &IQuery
which requires that Self
is Sized
, since Self
is a ‘bare’ trait (not behind a reference) it is not Sized
.
To me it was not entirely clear how to proceed from here. You can implement methods on the trait object type itself through the syntax impl<'a> IQuery + 'a { }
but now you’ve just inverted the problem: those methods are only available on trait objects!
Attempting to define them both in the trait and on the trait object makes the compiler yell loudly at you: playground
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0034]: multiple applicable items in scope
--> <anon>:45:15
|
45 | assert_eq!(q.get("i"), Some(&42));
| ^^^ multiple `get` found
|
note: candidate #1 is defined in an impl for the type `IQuery`
--> <anon>:13:2
|
13 | fn get<'s, T: 'static>(&'s self, name: &str) -> Option<&'s T> {
| __^ starting here...
14 | | self.query(name).and_then(|val| val.downcast_ref())
15 | | }
| |__^ ...ending here
note: candidate #2 is defined in the trait `IQuery`
--> <anon>:6:2
|
6 | fn get<'s, T: 'static>(&'s self, name: &str) -> Option<&'s T>
| __^ starting here...
7 | | where Self: Sized
8 | | {
9 | | self.query(name).and_then(|val| val.downcast_ref())
10 | | }
| |__^ ...ending here
= help: to disambiguate the method call, write `IQuery::get(&q, "i")` instead
I’m unsure how to describe the solution so I’ll just let the code speak for itself: playground
impl<'a, T: 'a + ?Sized + IQuery> IQuery for &'a T {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any> {
IQuery::query(*self, name)
}
}
We’re almost there. The above block implements the IQuery
interface for all references to T
implementing IQuery
. The ?Sized
bound means this also applies to trait objects!
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0277]: the trait bound `IQuery: std::marker::Sized` is not satisfied
--> <anon>:45:15
|
45 | assert_eq!(q.get("i"), Some(&42));
| ^^^ the trait `std::marker::Sized` is not implemented for `IQuery`
|
= note: `IQuery` does not have a constant size known at compile-time
Drat! What’s going on here is that Rust is selecting the wrong IQuery
implementation. You can fix this by calling (&q).get
…
Another solution is to let your Self: Sized
bounded methods take self
(in which case the Self
type will be resolved to &IQuery
when going through the blanket impl). However since there’s a lifetime involved this isn’t possible here…
And that’s where I’m stuck for now. If anyone has an idea to get past this last hurdle, do let me know!
The final working code, with workaround: playground
use ::std::any::{Any};
trait IQuery {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any>;
fn get<'s, T: 'static>(&'s self, name: &str) -> Option<&'s T>
where Self: Sized
{
self.query(name).and_then(|val| val.downcast_ref())
}
}
impl<'a, T: 'a + ?Sized + IQuery> IQuery for &'a T {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any> {
IQuery::query(*self, name)
}
}
struct Parameters {
i: i32,
f: f64,
s: String,
}
impl IQuery for Parameters {
fn query<'s>(&'s self, name: &str) -> Option<&'s Any> {
match name {
"i" => Some(&self.i),
"f" => Some(&self.f),
"s" => Some(&self.s),
_ => None,
}
}
}
fn main() {
let p = Parameters {
i: 42,
f: 3.1415927,
s: String::from("Hello world!"),
};
assert_eq!(p.get("i"), Some(&42));
assert_eq!(p.get("f"), Some(&3.1415927));
assert_eq!(p.get("s"), Some(&String::from("Hello world!")));
let q = &p as &IQuery;
assert_eq!((&q).get("i"), Some(&42));
assert_eq!((&q).get("f"), Some(&3.1415927));
assert_eq!((&q).get("s"), Some(&String::from("Hello world!")));
}