【问题标题】:How to create a vector of objects that share a concept?如何创建共享概念的对象向量?
【发布时间】:2021-04-20 02:00:01
【问题描述】:

我想创建一个包含不同类型但都共享相同概念的对象的向量(或数组)。
类似于 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;
}

【问题讨论】:

  • 不,C++ 不能这样工作。
  • 将概念从一种语言带到另一种语言通常是个坏主意。
  • 您很可能走错了方向。您可能正在寻找多态性。

标签: c++ c++20 c++-concepts


【解决方案1】:

您要查找的内容通常称为“类型擦除”。 C++20 concepts 语言特性不(也不能)支持类型擦除——该特性完全限于约束模板(类模板、函数模板、类模板的成员函数等)并且不能真正使用在任何其他情况下。

您必须改为手动编写您的类型已擦除 Talkable 或求助于使用可用的类型擦除库之一。

例如,dyno(Louis Dionne 在CppCon 2017CppNow 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 EvilC++ Seasoning 谈话,每个人都应该看到。这演示了一种执行运行时多态性的机制。
  • Sy Brand 的带有元类的动态多态性(他们在多个会议上发表了这个演讲,最近一次是 ACCU 2021)。这演示了如何使用反射工具编写类型擦除库,从而使总体代码比我上面展示的要少得多。

【讨论】:

  • 在 C++23 中提及 github.com/cplusplus/reflection-ts 可能会令人振奋。将 struct talker { void talk(); } 与反射转换为概念和类型擦除概念图类型是合理的,我上次戳反射。
  • @Yakk-AdamNevraumont 好主意 - 我放了一个 Sy 演讲的链接,该链接演示了如何做到这一点。
【解决方案2】:

你不能创建不同类型的向量。这种情况一般使用多态基类而不是使用概念来处理,例如:

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

    【讨论】:

    • OP 想要创建一个单一类型的向量 - 他们正在询问如何在 C++ 中创建像 Box&lt;dyn Trait&gt; 这样的类型。这是 Rust 中的一种类型。
    【解决方案4】:

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

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

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

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

    我们不知道。然而。

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

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

    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&lt;T&gt;clonable&lt;D,B&gt;,给我们:

    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,并带有一些额外的语法糖。

    现在,没有可以让您采用您定义的概念并生成任何代码。

    here 上,我遇到了类似的问题,我们有一个带有开和关概念的灯。

    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 类型 easure:

    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 在我上面链接的演讲中使用虚函数来减少样板文件(以无法以可移植方式创建免分配引用为代价)。

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

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

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

    如果你想要侵入性(让 dog/cat 继承自 Talker),那么https://stackoverflow.com/a/49546808/1774667 是一个简单的版本。

    对于类似这样的更高级的语法:

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

    你可以使用Type erasing type erasure, `any` questions?

    还有更高级的版本。


    这太丑了。

    正确的做法是从头开始

    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++ 中的概念既是测试,又是太强大,它们不会公开正确的信息来生成代码以使某些东西通过测试。

    【讨论】:

      【解决方案5】:

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

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

      您也可以使用继承,如另一个答案所示,但这是在您的类型不通过继承相关的情况下。

      【讨论】:

      • @Barry 非常感谢。几个问题:这是static vtable 线程安全的吗?为什么让destroy 采用非常量指针会更好?
      • 是的,您不需要或不希望每个线程有不同的实例。什么时候需要destroy 来获取 const 对象?
      • @Barry 因为delete 使用 const 指针,所以我将其设为 const,但我同意您的版本更合理。
      【解决方案6】:

      您可以使用 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;
      }
      

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

      八卦

      喵喵

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

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

      【讨论】:

      • 您可以使用godbolt 进行在线演示。它拥有最常用编译器的最新版本。
      • @cigien 为什么要打扰?为您的努力再次获得匿名反对票?
      • 我只是分享了它,以防您想使用在线编译器添加演示,因为您在回答中提到了它。另外,当他们是匿名的时,我不会担心会被否决。这让我个人感到沮丧,因为我不知道我的帖子在匿名时有什么问题,但我知道对此无能为力。
      • 如果你有一组封闭的类型,variant 是一个很好的解决方案——但这不是这里的问题。 Box&lt;dyn Trait&gt; 不是变体。它是一个类型擦除对象,其接口匹配Trait,否则其行为类似于unique_ptr
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-08-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多