Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a vector of objects that share a concept?

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;
}
like image 295
kirbylife Avatar asked Dec 13 '22 07:12

kirbylife


2 Answers

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.
like image 150
Barry Avatar answered Jan 30 '23 21:01

Barry


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

like image 37
Remy Lebeau Avatar answered Jan 30 '23 23:01

Remy Lebeau