B_old
B_old

Reputation: 1291

Is it possible for a trait to return trait objects specified by a generic type on the trait?

I would like to define a trait like this:

pub trait Provider<T> {
    fn load(&self, name: &str) -> Box<dyn T>;
}

But it is not possible:

error[E0404]: expected trait, found type parameter `T`
 --> src/lib.rs:2:47
  |
2 |         fn load(&self, name: &str) -> Box<dyn T>;
  |                                               ^ not a trait

A possible solution is to remove the dyn keyword, but I want implementors of Provider to return trait objects, like this:

pub struct MaterialProvider {}

trait Material {}

impl Provider<Material> for MaterialProvider {
    fn load(&self, name: &str) -> Box<dyn Material> { /*...*/ }
}

Is it possible to express such a concept?

I wanted to use this to create a single "resource repository" which can be used to create Objects of varying types. The exact types which should be supported might not be known at compile time and they don't necessarily need to implement the same interface.

In C++ one way to solve the described problem is like this:

#include <iostream>
#include <map>

class A {
  public:
    static std::string identifier()  {
        return "A";
    }
};

class B {
  public:
    static std::string identifier()  {
        return "B";
    }
};

class ProviderBase {};

template <typename T>
class Provider : public ProviderBase {
  public:
    virtual T* load() = 0;
};

class ProviderA : public Provider<A> {
  public:
    A* load() {
        return new A;
    }
};

class Manager {
    std::map<std::string, ProviderBase*> providers;

  public:
    template<typename T>
    void register_provider(Provider<T>* provider) {
        providers[T::identifier()] = provider;
    }

    template<typename T>
    T* load() {
        auto p = providers.find(T::identifier());

        if (providers.end() != p) {
            return static_cast<Provider<T>*>(p->second)->load();
        }

        return nullptr;
    }
};

int main(int argc, char* argv[]) {
    Manager m;

    ProviderA provider_a;

    m.register_provider(&provider_a);

    if (m.load<A>()) {
        std::cout << "Loaded A" << std::endl;
    } else {
        std::cout << "Could not load A" << std::endl;
    }

    if (m.load<B>()) {
        std::cout << "Loaded B" << std::endl;
    } else {
        std::cout << "Could not load B" << std::endl;
    }

    return 0;
}

Upvotes: 1

Views: 672

Answers (1)

Anders Kaseorg
Anders Kaseorg

Reputation: 3875

A generic trait may be parameterized on a type, but not on another trait. So in trait Provider<T>, T is a type, not a trait, which means there’s no such thing as dyn T.

But a type parameter should be all you need here, if the type is itself allowed to be a trait object type. To allow this, you just need to suppress the default Sized bound, since trait objects are unsized:

pub trait Provider<T: ?Sized> {
    fn load(&self, name: &str) -> Box<T>;
}

pub struct MaterialProvider {}

trait Material {}

impl Provider<dyn Material> for MaterialProvider {
    fn load(&self, name: &str) -> Box<dyn Material> { /* ... */ }
}

Upvotes: 1

Related Questions