4

I want to create a vector (or array) of objects of different types but all sharing the same concept.
similar to Vec<Box<dyn trait>> of Rust.

struct Dog {
    void talk() {
        std::cout << "guau guau" << std::endl;
    }
};

struct Cat {
    void talk() {
        std::cout << "miau miau" << std::endl;
    }
};

template <typename T>
concept Talk = requires(T a) {
    { a.talk() } -> std::convertible_to<void>;
};

int main() {
    auto x = Dog{};
    auto y = Cat{};

    ??? pets = {x, y};

    for(auto& pet: pets) {
        pet.talk();
    }

    return 0;
}
cigien
  • 57,834
  • 11
  • 73
  • 112
kirbylife
  • 115
  • 1
  • 9

6 Answers6

11

What you're looking for is typically called "type erasure." The C++20 concepts language feature does not (and cannot) support type erasure - that feature is limited entirely to constraining templates (class templates, function templates, member functions of class templates, etc.) and cannot really be used in any other context.

You'll have to instead either write your type erased Talkable by hand or resort to using one of the available type erasure libraries.

For example, with dyno (which Louis Dionne gave several talks on CppCon 2017, CppNow 2018), this would look as follows. You'll note that the only place I'm using the concept Talk is to constrain the default concept map:

#include <dyno.hpp>
#include <vector>
#include <iostream>
using namespace dyno::literals;

// this is the "concept" we're going to type erase
struct PolyTalkable : decltype(dyno::requires_(
    dyno::CopyConstructible{},
    dyno::Destructible{},
    "talk"_s = dyno::method<void()>
)) { };

template <typename T>
concept Talk = requires (T a) { a.talk(); };

// this how we implement our "concept"
template <Talk T>
auto const dyno::default_concept_map<PolyTalkable, T> = dyno::make_concept_map(
    "talk"_s = [](T& self) { self.talk(); }
);

// this is our hand-written "dyn PolyTalkable"
class DynTalkable {
    dyno::poly<PolyTalkable> impl_;
public:
    template <typename T>
        requires (!std::same_as<T, DynTalkable>
               && dyno::models<PolyTalkable, T>())
    DynTalkable(T t) : impl_(t) { }

    void talk() {
        impl_.virtual_("talk"_s)();
    }
};

struct Dog {
    void talk() {
        std::cout << "guau guau" << std::endl;
    }
};

struct Cat {
    void talk() {
        std::cout << "miau miau" << std::endl;
    }
};

int main() {
    std::vector<DynTalkable> pets;
    pets.push_back(Dog{});
    pets.push_back(Cat{});
    for (auto& pet : pets) {
        pet.talk();
    }
}

For other resources on C++ type erasure, see also:

  • Sean Parent's Inheritance is the Base Class of Evil and C++ Seasoning talk, which everyone should see anyway. This demonstrates a mechanism for doing runtime polymorphism.
  • Sy Brand's Dynamic Polymorphism With Metaclasses (they gave this talk at multiple conferences, most recently ACCU 2021). This demonstrates how to do write a type erasure library using reflection facilities so that the result is much less overall code than what I showed above.
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Mentioning https://github.com/cplusplus/reflection-ts in C++23 might be enheartening. Converting `struct talker { void talk(); }` with reflection into both a concept and a type erasing concept map type was plausible last ti e I poked at reflection. – Yakk - Adam Nevraumont Apr 20 '21 at 04:08
  • @Yakk-AdamNevraumont Good idea - I put a link to one of Sy's talks that demonstrates how to do just that. – Barry Apr 20 '21 at 13:27
2

You can’t create a vector of different unrelated types. This kind of situation is generally handled using a polymorphic base class rather than using concepts, eg:

struct Animal {
    virtual void talk() = 0;
};

struct Dog : Animal {
    void talk() override {
        std::cout << "guau guau" << std::endl;
    }
};

struct Cat : Animal {
    void talk() override {
        std::cout << "miau miau" << std::endl;
    }
};

int main() {
    auto x = Dog{};
    auto y = Cat{};

    std::vector<Animal*> pets{&x, &y};

    for(auto& pet : pets) {
        pet->talk();
    }

    return 0;
}

Demo

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
0

Constraints are a way to protect a template from being instantiated with types that don't fulfill certain requirements. They do not make the language into a different language, nor give types the power to do something they couldn't do before.

vector is an array of objects of the same type. Constraints don't change that. A constraint can allow you to make sure a user-provided type fits a requirement that your code expects. But vector will ultimately always contain objects of the same type. Because that's what it is.

Similarly, C++ is a statically typed language. This means that the type of everything needs to be known at compile-time. So if you have a loop, the type of things within that loop cannot be dependent on the loop counter or any other runtime construct.

This also doesn't change because of concepts.

What you want is not possible. There may eventually be compile-time loops that get unrolled such that different expressions can take on different types, but that would be to allow "looping" over the elements of a tuple.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    OP wants to create a vector of a single type though - they're asking how to create a type like `Box` in C++. That is a type in Rust. – Barry Apr 20 '21 at 02:56
0

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_anys 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 .


TL;DR: there is no trivial or built-in way to do this in .

You can implement it, or use libraries, to reduce the boilerplate, and get syntax similar to it.

In 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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

You can do type erasure without third party libraries. Here's my go at it:

#include <algorithm>  // for max
#include <cstdio>     // for printf
#include <utility>    // for exchange, move
#include <vector>     // for vector

// Classes that "Implement" Walker/Talker
struct Dog {
  int i;
  void Talk() const { std::printf("Dog #%d Talks\n", i); }
  void Walk() const { std::printf("Dog #%d Walks\n", i); }
};

struct Cat {
  int i;
  void Talk() const { std::printf("Cat #%d Talks\n", i); }
  void Walk() const { std::printf("Cat #%d Walks\n", i); }
};

// Type-erased "smart reference"
class WalkerTalker {
 private:
  struct VTable {
    void (*talk)(void const*);
    void (*walk)(void const*);
    void (*destroy)(void*) noexcept;
    void* (*copy)(void const*);
  };

  VTable const* _vtable = nullptr;
  void* _data = nullptr;

  template <typename T>
  static constexpr VTable vtable_for{
      .talk = [](void const* vp) { static_cast<T const*>(vp)->Talk(); },
      .walk = [](void const* vp) { static_cast<T const*>(vp)->Walk(); },
      .destroy = [](void* vp) noexcept { delete static_cast<T*>(vp); },
      .copy = [](void const* vp) -> void* {
        return new T(*static_cast<T const*>(vp));
      }};

  template <typename U>
  void Assign(U&& u) {
    CleanUp();

    using T = std::remove_cvref_t<U>;
    _vtable = &vtable_for<T>;
    _data = new T(std::forward<U>(u));
  }

  void CleanUp() noexcept {
    if (_data) _vtable->destroy(std::exchange(_data, nullptr));
  }

 public:
  // Dispatch calls to the vtable
  void Talk() const { _vtable->talk(_data); }
  void Walk() const { _vtable->walk(_data); }

  // ... interface to manage the assignment and object life time and stuff ...
  ~WalkerTalker() { CleanUp(); }

  template <typename T>
  requires(!std::same_as<std::remove_cvref_t<T>, WalkerTalker>)
      WalkerTalker(T&& t) {
    Assign(std::forward<T>(t));
  }

  template <typename T>
  requires(!std::same_as<std::remove_cvref_t<T>, WalkerTalker>) WalkerTalker&
  operator=(T&& t) {
    Assign(std::forward<T>(t));
    return *this;
  }

  WalkerTalker(WalkerTalker const& other) : _vtable(other._vtable) {
    if (other._data) _data = other._vtable->copy(other._data);
  }
  
  WalkerTalker& operator=(WalkerTalker const& other) {
    if (this != &other) {
      CleanUp();
      if (other._data) {
        _vtable = other._vtable;
        _data = other._vtable->copy(other._data);
      }
    }

    return *this;
  }

  WalkerTalker(WalkerTalker&& other) noexcept
      : _vtable(std::exchange(other._vtable, nullptr)),
        _data(std::exchange(other._data, nullptr)) {}

  WalkerTalker& operator=(WalkerTalker&& other) noexcept {
    if (this != &other) {
      CleanUp();
      _vtable = std::exchange(other._vtable, nullptr);
      _data = std::exchange(other._data, nullptr);
    }
    return *this;
  }
};

int main() {
  std::vector<WalkerTalker> vec;

  // Example data
  for (int i = 0; i != 100; ++i) {
    if (i & 1)
      vec.push_back(Dog{i});
    else
      vec.push_back(Cat{i});
  }

  for (auto const& elm : vec) elm.Talk();
}

You could also use inheritance as shown in another answer, but this is in case your types are not related through inheritance.

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • @Barry much appreciated. A couple questions: Is this `static` vtable thread-safe? And why is it a better idea to make `destroy` take a non-const pointer? – Aykhan Hagverdili Apr 20 '21 at 20:14
  • Yes, and you don't need or want different instances per thread. And when do you need `destroy` to take a const object? – Barry Apr 20 '21 at 20:55
  • @Barry since `delete` works on const pointers, I made it const, but I agree that your version is more sensible. – Aykhan Hagverdili Apr 20 '21 at 21:00
-1

You can implement that using std::variant from C++17:

struct Dog {
    void talk() {
        std::cout << "guau guau" << std::endl;
    }
};

struct Cat {
    void talk() {
        std::cout << "miau miau" << std::endl;
    }
};

using animals = std::vector<std::variant<Dog,Cat>>;

int main() {
        animals v = { Dog(), Cat() };
        auto visitor = []( auto &&a ) { a.talk(); };
        for( auto &&a : v )
            std::visit( visitor, a );

        return 0;
}

Here is output from code on my computer:

guau guau

miau miau

(unfortunately I am not aware of online compilers like ideone.com that supports C++17 or newer to provide live code)

but concept does not add anything here for functionality. I guess you can use concept for visitor parameter validation here, but benefit of that use would be questionable.

Slava
  • 43,454
  • 1
  • 47
  • 90
  • 1
    You can use [godbolt](https://godbolt.org/z/EY498143G) for online demos. It's got the latest versions of most commonly used compilers. – cigien Apr 20 '21 at 02:30
  • @cigien why to bother? to get another anonymous downvote for your effort? – Slava Apr 20 '21 at 02:31
  • 1
    I just shared it in case you want to add demos with online compilers, since you mentioned it in your answer. Also, I wouldn't worry about getting downvotes when they're anonymous. It frustrates me personally, since I don't know what's wrong with my post when it's anonymous, but I've learned that there's not much that can be done about it. – cigien Apr 20 '21 at 02:34
  • 4
    If you have a closed set of types, variant is a good solution - but that's not the problem at hand here. `Box` is not a variant. It is a type erased object whose interface matches `Trait` and otherwise behaves like a `unique_ptr`. – Barry Apr 20 '21 at 02:55