So C++ concepts are not the same as Rust's.
So C++ supports concepts, but it does not support concept maps or automatic type erasure.
concept maps take types, and map them to the concept. Type erasure takes a type, and a concept, and forgets everything about the type besides the parts that are required to fulfill the concept.
What you appear to want is automatic type erasure of value types based on a concept.
We don't got that. Yet.
There are many examples of type erasure in C++. The most common one is virtual inheritance, where you have an interface class, and then pointers to the derived types are erased down to pointers to the interface class and its methods.
This is cludgy and annoying, but it is easy to use.
struct ITalker {
virtual void talk() const = 0;
};
You probably also want value semantics. Minimal support for that looks like:
struct ITalker {
virtual void talk() const = 0;
virtual std::unique_ptr<ITalker> clone() const = 0;
virtual ~ITalker() {}
};
then we can brew up a clone_ptr<T>
and clonable<D,B>
, giving us:
struct Dog:clonable<Dog, ITalker> {
void talk() const final {
std::cout << "guau guau" << std::endl;
}
};
struct Cat:clonable<Cat, ITalker> {
void talk() const final {
std::cout << "miau miau" << std::endl;
}
};
template <typename T>
concept Talk = requires(T a) {
{ a.talk() } -> std::convertible_to<void>;
};
auto x = Dog{};
auto y = Cat{};
std::vector<std::clone_ptr<ITalker>> pets = {x, y};
for(auto& pet: pets) {
if(pet)
pet->talk();
}
with a bunch more boilerplate.
This is, however, not what you want; it is intrusive for one.
We can go further and clean up the pointer-ness with boilerplate. That can get you
std::vector<Talker> pets = {x, y};
for(auto& pet: pets) {
pet.talk();
}
where it looks more like values. Under the hood it ends up being the clone_ptr
above with some extra syntactic sugar.
Now, none of this lets you take the concept you defined and generate any of this code.
Over here I got through a similar problem, where we have a light with the concept of on and off.
namespace Light {
struct light_tag{};
template<class T>
concept LightClass = requires(T& a) {
{ a.on() };
{ a.off() };
};
void on(light_tag, LightClass auto& light){ light.on(); }
void off(light_tag, LightClass auto& light){ light.off(); }
// also, a `bool` is a light, right?
void on(light_tag, bool& light){ light=true; }
void off(light_tag, bool& light){ light=false; }
template<class T>
concept Light = requires(T& a) {
{ on( light_tag{}, a ) };
{ off( light_tag{}, a ) };
};
void lightController(Light auto& l) {
on(light_tag{}, l);
off(light_tag{}, l);
}
struct SimpleLight {
bool bright = false;
void on() { bright = true; }
void off() { bright = false; }
};
}
The above has the concept of "are you a light", but uses the light_tag
to allow other types to "count as lights" either by having .on
and .off
methods, or supporting calling on(light_tag, foo)
and off(light_tag, foo)
.
I then go on to implement Sean-parent ish type easure on top of this:
namespace Light {
struct PolyLightVtable {
void (*on)(void*) = nullptr;
void (*off)(void*) = nullptr;
template<Light T>
static constexpr PolyLightVtable make() {
using Light::on;
using Light::off;
return {
[](void* p){ on( light_tag{}, *static_cast<T*>(p) ); },
[](void* p){ off( light_tag{}, *static_cast<T*>(p) ); }
};
}
template<Light T>
static PolyLightVtable const* get() {
static constexpr auto retval = make<T>();
return &retval;
}
};
struct PolyLightRef {
PolyLightVtable const* vtable = 0;
void* state = 0;
void on() {
vtable->on(state);
}
void off() {
vtable->off(state);
}
template<Light T> requires (!std::is_same_v<std::decay_t<T>, PolyLightRef>)
PolyLightRef( T& l ):
vtable( PolyLightVtable::get<std::decay_t<T>>() ),
state(std::addressof(l))
{}
};
}
adapting that to .talk()
is pretty easy.
Adding value semantics, and the resulting polymorphic value type can be stored in a std::vector
; but as you can see, there is boilerplate to write.
Sean Parent uses virtual functions to reduce the boilerplate (at the cost of not being able to create allocation-free references in a portable manner) in the talk I linked above.
So far so good. You can do it. Welcome to the turing tar pit; the problem is, it isn't easy.
To make it easy, you either need to do some serious metaprogramming, find someone who did it before.
I, for example, have written multiple poly_any
s that automate some of this.
If you want to be intrusive (make dog/cat inherit from Talker), then https://stackoverflow.com/a/49546808/1774667 is a simple version.
For fancier syntax sort of like this:
auto talk = make_any_method<void()>{ [](auto& obj){ obj.talk(); };
std::vector< super_any<decltype(talk)> > vec{ Dog{}, Cat{} };
for (auto& e:vec) {
(e->*talk)();
}
You can use Type erasing type erasure, `any` questions?
Even fancier versions exist.
This is all ugly as heck.
The right way to do it is to start with
struct Talker {
void talk();
};
which fully describes the concept, then do something like:
using AnyTalker = TypeErase::Value<Talker>;
and
std::vector<AnyTalker> vec{ Cat{}, Dog{} };
for (auto const& e:vec)
e.talk();
but that will have to wait until c++23.
TL;DR: there is no trivial or built-in way to do this in c++20.
You can implement it, or use libraries, to reduce the boilerplate, and get syntax similar to it.
In c++23 we expect to be able to remove most of the boilerplate in order to do this. The syntax doesn't exactly match yours; it uses a struct as a prototype, instead of a concept.
In no cases is the type erasing derived from a concept like you wrote. Concepts in C++ are both tests and are too powerful, they do not expose the correct information to generate code to make something pass the test.