如何创建共享概念的对象向量?

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

我想创建一个包含不同类型但共享相同概念的对象的向量(或数组)。
类似于 Rust 的 Vec<Box<dyn trait>>

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;
}

约束是一种保护模板不被不满足特定要求的类型实例化的方法。他们不会将语言变成另一种语言,也不会赋予类型做他们以前做不到的事情的能力。

vector 是相同类型对象的数组。约束不会改变这一点。约束可以让您确保用户提供的类型符合您的代码预期的要求。但是 vector 最终将始终包含相同类型的对象。因为它就是这样。

同样,C++ 是一种静态类型语言。这意味着在编译时需要知道所有内容的类型。因此,如果您有一个循环,则该循环中的事物类型不能依赖于循环计数器或任何其他运行时构造。

这也不会因为概念而改变。

你想要的是不可能的。最终可能会有编译时循环展开,这样不同的表达式可以采用不同的类型,但这将允许“循环” tuple.

的元素

您可以使用 C++17 中的 std::variant 来实现:

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;
}

这是我电脑上代码的输出:

guau guau

miau miau

(不幸的是,我不知道像 ideone.com 这样支持 C++17 或更新版本以提供实时代码的在线编译器)

但是概念并没有在这里添加任何功能。我猜你可以在这里使用概念来验证访问者参数,但使用的好处值得怀疑。

您要找的通常称为“类型擦除”。 C++20 concept 的语言特性不(也不能)支持类型擦除——该特性完全限于约束模板(class 模板、函数模板、class 的成员函数模板等),不能真正用于任何其他上下文。

您将不得不改为手动编写擦除类型 Talk,或者使用可用的类型擦除库之一。

例如,对于 dyno (which Louis Dionne gave several talks on CppCon 2017, CppNow 2018),这将如下所示。您会注意到我唯一使用概念 Talk 的地方是限制默认概念图:

#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();
    }
}

有关 C++ 类型擦除的其他资源,另请参阅:

  • Sean Parent 的 Inheritance is the Base Class of Evil and C++ Seasoning 演讲,无论如何每个人都应该看看。这演示了执行运行时多态性的机制。
  • Sy Brand 的 Metaclasses 动态多态性(他们在多个会议上发表了这个演讲,最近一次是 ACCU 2021)。这演示了如何使用反射工具编写类型擦除库,从而使结果比我上面显示的代码少得多。

您不能创建不同的不相关类型的向量。这种情况一般使用多态基class来处理,而不是使用概念,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

所以 C++ 概念与 Rust 的概念不同。

所以C++支持概念,但不支持概念图和自动类型擦除。

概念图采用类型,并将它们映射到概念。类型擦除接受一个类型和一个概念,并忘记除了实现该概念所需的部分之外的关于该类型的所有内容。

您似乎想要的是基于概念的值类型的自动类型擦除。

我们不明白。然而

C++ 中有很多类型擦除的例子。最常见的一种是虚拟继承,其中有一个接口 class,然后指向派生类型的指针被擦除为指向接口 class 及其方法的指针。

这既笨拙又烦人,但很容易使用。

struct ITalker {
  virtual void talk() const = 0;
};

您可能还需要值语义。对此的最低支持看起来像:

struct ITalker {
  virtual void talk() const = 0;
  virtual std::unique_ptr<ITalker> clone() const = 0;
  virtual ~ITalker() {}
};

然后我们可以酿造一个clone_ptr<T>clonable<D,B>,给我们:

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();
}

还有一堆样板文件。

然而,这不是您想要的;这对一个人来说是侵入性的。

我们可以 go further 并使用样板清理指针。那可以让你

 std::vector<Talker> pets = {x, y};

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

它看起来更像是价值观。在引擎盖下,它最终成为上面的 clone_ptr 和一些额外的语法糖。

现在,none 允许您使用您定义的概念并生成任何此代码。

结束 我遇到了类似的问题,其中我们有一个带有开和关概念的灯。

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; }
  };
}

上面有“你是灯”的概念,但使用 light_tag 允许其他类型通过 .on.off 来“算作灯”方法,或支持调用 on(light_tag, foo)off(light_tag, foo).

然后我继续在此之上实现 Sean-parent ish 类型的缓和:

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))
    {}
  };
}

将其适应 .talk() 非常容易。

添加值语义,生成的多态值类型可以存储在std::vector;但如您所见,有样板可写。

Sean Parent 在我上面链接的演讲中使用虚函数来减少样板文件(代价是无法以可移植的方式创建免分配引用)。

到目前为止一切顺利。你能行的。欢迎来到图灵tar坑;问题是,这并不 容易

为了简单起见,您要么需要进行一些认真的元编程,要么找以前做过的人。

例如,我已经编写了多个 poly_anys 来自动化其中的一些。

如果想打扰一下(让dog/cat继承Talker),那么就是一个简单的版本。

更高级的语法如下:

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)();
}

您可以使用

甚至还有更高级的版本。


这太丑了。

正确的做法是 start with

struct Talker {
  void talk();
};

完整地描述了这个概念,然后做类似的事情:

using AnyTalker = TypeErase::Value<Talker>;

std::vector<AnyTalker> vec{ Cat{}, Dog{} };
for (auto const& e:vec)
  e.talk();

但这必须等到


TL;DR:在 .

中没有简单或内置的方法可以做到这一点

您可以实施它或使用库来减少样板文件,并获得与其相似的语法。

中,我们希望能够删除大部分样板文件以实现此目的。语法不完全符合你的;它使用结构作为原型,而不是概念。

在任何情况下,类型擦除都不是像您写的那样从概念中派生出来的。 C++ 中的概念都是测试并且太强大,它们没有公开正确的信息来生成代码来使某些东西通过测试。

您可以在没有第三方库的情况下进行类型擦除。这是我的做法:

#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();
}

您也可以使用继承,如另一个答案所示,但这是为了防止您的类型通过继承不相关。