基类指针容器的虚方法的等价物?

时间:2015-03-06 21:44:35

标签: c++ polymorphism c++14

注意:这篇文章对代码很轻松,因为它专注于概念;我打算根据这里得出的结论重写相关代码的相当大部分内容,因此显示大量代码只会使帖子更长,IMO没有充分理由。

要点:

我不认为我可以编写一个单独使用的简短版本(我试过,它根本不短),所以我将从主要摘要开始目标。

我需要在对象指针容器上实现虚拟方法 目前,我有类似virtual object::performAction(action someAction)的东西,它适用于一次调用一个对象。但是,我现在需要一个等价物,可以同时对object的多个实例执行操作(其中实例可以是各种派生类,混合在一起)。也就是说,概念上等同于virtual vector<object*>::performAction(action someAction)的东西;该方法需要访问所有object一次,因此在向量上的foreach循环中调用当前实现不会切断它。

由于下面详述的原因,我不相信我可以简单地创建一个虚拟方法,例如action::performAction(vector<object*>),至少不能以干净且安全的方式创建。

详细信息:

我正在开发一种应用程序,它是一种启动器/本地搜索应用程序,允许对各种项目进行按类型搜索。
它有两个重要的概念:
1)对象(例如歌曲,文件,应用程序),或更一般地说:用户可能搜索的内容,您可以执行某种操作。这些是抽象基类的子类,称之为object 2)动作(例如,播放,入队,使用默认应用程序打开,运行,在文件管理器中显示):用户可能想要对对象执行的操作。这些目前由字符串表示,但可能需要更改以解决此问题。

现在,您一次只能选择一个对象。选择对象后,调用虚拟方法selectedObject->getActions();显示这些操作,用户从该列表中选择一个操作,最后调用selectedObject->performAction(selectedAction)之类的内容 我想扩展它,以便您可以对多个对象执行一次操作。也就是说,用户选择1个或更多对象,选择适用于所有对象的操作,并且操作一次对所有对象执行。
我想我将不得不翻转它:而不是selectedObject->performAction(selectedAction),我可能必须做selectedAction->performActionOnObjects(selectedObjects),其中selectedAction是一个基类指针,每种类型的动作都是它自己的动作子类({ {1}},playactionenqueueaction等)。

但是,我不确定如何实现这一点。每个动作只应接受一些对象类型:您可以播放歌曲,但不能播放应用程序。另一方面,给定的动作应该允许多种对象类型:您可以播放歌曲和专辑,但歌曲和专辑则表示为不同的runaction子类。

我能想到的唯一方法是使用接口(C ++中的多重继承?),即object派生自albumobject和某种object - ish抽象类,但我不确定如何在实践中实现这一点。我也不知道这是否是解决这个问题的好方法 具体来说,我不确定如何确定哪个IPlayable实现IPlayable,在运行时(没有强制转换),或者如何在没有这些知识的情况下实现。
如果这是采取的道路,在我看来,每个&#34;界面&#34;需要声明获取特定于动作的信息的方法(专辑的歌曲URL,执行应用程序的路径+参数等)。这里是渔获物22.
如果object之类的操作将playaction::performActionOnObjects作为参数,它将接受所有对象类,无论&#34;播放&#34;行动对他们有意义。
如果相反它需要接口指针,例如vector<object*>,我就不知道如何正确地调用它,因为我在呼叫站点所拥有的只是playaction::performActionOnObjects(vector<IPlayable*>)的向量。其中一些可能会实现IPlayable,但许多人不会。我可以很容易地找出实现该操作的方法(通过object*上的虚拟方法),但是必须将它们转换为调用操作方法,对吗?

因此,简而言之:
*每个object可能有多个动作(例如专辑有游戏,入队和其他几个) *每个object可能支持也可能不支持多种action类型(您可以播放歌曲和专辑,但唯一可运行的对象类型是应用程序)
*我们想要操作的所有对象,无论何种类型,都存储在一个基本指针容器中(具体为object

如何以干净的方式实现这一点? 优选地,未来的应用程序插件可以在不触及主应用程序代码的情况下定义附加动作和/或对象类型。 我一直在阅读有关访客模式的一些内容,但并不完全&#34;得到&#34;它还没有,所以我不确定它是否适用于此,如果适用,如何实施它。

更新

这是一些示例代码。请注意,这可能是错误的方法,问题是如何这样做。

QVector<shared_ptr<object>>

如果我改为定义
#include <vector> #include <string> using namespace std; // Classes which implement this can be sent to playaction::performActionOnObjects(...) class IPlayable { public: virtual vector<string> getTrackURLs() = 0; }; class object { public: virtual string getName() = 0; }; class albumobject : public object, public IPlayable { public: string getName() override { return "some album"; } vector<string> getTrackURLs() override { return { "file:///track/1", "file:///track/2" }; }; }; class applicationobject : public object { public: string getName() override { return "some application"; } string getExecutablePath() { return "/usr/bin/application"; } }; class action { public: virtual void performActionOnObjects(vector<object*> objects) = 0; }; class playaction : public action { public: void performActionOnObjects(vector<object*> objects) override { vector<string> allUrls; for (object *obj : objects) { // Not possible: object doesn't implement getTrackURLs // dynamic_cast to IPlayable* required; is there a better way? auto tmp = obj->getTrackURLs(); allUrls.insert(end(allUrls), begin(tmp), end(tmp)); } // Do something with allUrls... } }; int main() { vector<object*> objects { new albumobject, new albumobject }; playaction action; action.performActionOnObjects(objects); return 0; }
只是将问题转移到main(),我想将playaction::performActionOnObjects(vector<IPlayable*> objects)传递给需要vector<object*>的方法,除非我遗漏了一些至关重要的东西。
因此,我正在寻找一些方法来进一步解耦这一点,所以我可以避免在两个位置进行转换。

6 个答案:

答案 0 :(得分:0)

除非我误解了你的问题,否则我有一个可能的解决方案。

使用合成而非继承。

  • 使用bitset跟踪&#34;对象功能&#34;。
  • 对象可能具有或不具有特定组件(&#34;功能&#34;)。
  • 使用经理查询对象。

// An instance of this bitset will be stored in every object
// to keep track of its available components
constexpr std::size_t objectTypeCount{2};
using ObjectBitset = std::bitset<objectTypeCount>;

// Bit 0: ComponentPlayable availability 
constexpr std::size_t bitPlayable{0};

// Bit 1: ComponentExecutable availability 
constexpr std::size_t bitExecutable{1};

// Note: objects can be both playable and executable at the same time
//       using this component-based design

// A component is a class with data and logic
struct ComponentPlayable
{
    // Could also store a reference to the parent object here
    // to allow easier communication between components in the 
    // same object

    int lengthInSeconds;
    void play() { /* ... */ }
};

struct ComponentExecutable
{
    std::string targetArchitecture;
    bool permissionsAvailable;
    void execute() { /* ... */ }
};

// An object is just a container of components
struct Object
{
    ObjectBitset types;

    // Obviously a more clever way of storing/creating/querying
    // components could be used
    std::unique_ptr<ComponentPlayable> cPlayable;
    std::unique_ptr<ComponentExecutable> cExecutable;

    // Constructing and initializing a component sets the appropriate
    // bit to true
    template<typename... Ts> void initCPlayable(Ts&&... xs)
    {
        cPlayable = std::make_unique<ComponentPlayable>(std::forward<Ts>(xs)...);
        types[bitPlayable] = true;
    }

    template<typename... Ts> void initCExecutable(Ts&&... xs)
    {
        cExecutable = std::make_unique<ComponentExecutable>(std::forward<Ts>(xs)...);
        types[bitExecutable] = true;
    }

    // Object component availability can be checked quickly
    // thanks to the bitset
    bool isPlayable() { return types[bitPlayable]; }
    bool isExecutable() { return types[bitExecutable]; }

    // Note: checking component availability could have been done
    //       by comparing the stored unique_ptr with nullptr as well,
    //       but using bitset allows for quicker checks for specific
    //       object signatures (has this object component X, Y and Z?)
    //       and also assigning a specific unique bit to components 
    //       can make it much easier to design efficient data structures
    //       for component storage and object querying
};

struct Manager
{
    std::vector<Object> objects;

    // Could also dynamically add/remove objects to buckets
    // based on the components they have to speed up querying

    template<typename TF> void execOnPlayables(TF mFn)
    {        
        for(auto& o : objects) 
            if(o.isPlayable()) 
            { 
                assert(o.cPlayable != nullptr); 
                mFn(*o.cPlayable);
            }
    }

    template<typename TF> void execOnExecutables(TF mFn)
    {        
        for(auto& o : objects) 
            if(o.isExecutable()) 
            { 
                assert(o.cExecutable != nullptr); 
                mFn(*o.cExecutable);
            }
    }
};

int main()
{
    Manager m;
    // Fill manager here...

    m.execOnPlayables([](auto& mC){ mC.play(); });
    m.execOnExecutables([](auto& mC){ mC.execute(); });
}

面向组件的设计的一些好处:

  • 对象不必遵循特定的继承树
  • 对象可以有多个组件(功能)
  • 可以轻松地在其组件上查询对象(在可播放可执行文件的所有对象上执行此功能,但不是可移动

答案 1 :(得分:0)

getActions返回pair<action_id, std::function<void ()>的容器,函数对象已经绑定到派生得更多的子对象。

答案 2 :(得分:0)

您可以采取某种行动Visitor。访问者访问已选择要执行的所有项目,然后只要他们都满足要求,就会对所有项目执行操作:

#include <vector>
#include <iostream>

class Object;
class Application;
class Song;

struct IPlayable {
  virtual void play() = 0;
};

struct ActionVisitor {
  virtual void visit(Application&) = 0;
  virtual void visit(Song&) = 0; 
};

struct Object {
  virtual void accept(ActionVisitor&) = 0;
};
struct Application : Object {
  void accept(ActionVisitor& visitor) override { visitor.visit(*this); }
};
struct Song : IPlayable, Object {
  std::string name;

  Song(std::string name) : name(std::move(name)) {}
  void accept(ActionVisitor& visitor) override { visitor.visit(*this); }
  void play() override { std::cout << "Playing " << name << "\n"; }
};

class PlayVisitor : public ActionVisitor {
  std::vector<IPlayable*> queue;
  bool playable;
 public:
  void visit(Application&) override { playable = false; }
  void visit(Song& song) override { queue.push_back(&song); }
  void play(std::vector<Object*>& objects) {
    playable = true;
    queue.clear();
    for(auto& object : objects)
       object->accept(*this); 
    if (playable) {
      for(auto object : queue) object->play(); 
    }
  };
};

int main() {
  Song song1("Song1");
  Song song2("Song2");
  Application app1;

  std::vector<Object*> selected_playable{&song1, &song2};
  PlayVisitor player;
  player.play(selected_playable);

  std::vector<Object*> unplayable{&song2, &app1};
  player.play(unplayable);
}

Live demo

与访问者模式一样,访问者违反了open/closed原则,因为它必须具有所有可能对象的访问功能,但这可能是值得付出的代价。

答案 3 :(得分:0)

如果您不想区分对对象执行的操作类型,则只需将virtual void run()=0;添加到Base类并在其自己的类中实现对象特定的操作。
如果要对支持此操作的对象矢量对所有对象执行特定操作,则必须实现某种类型的功能测试。

#include <iostream>
#include <vector>
using namespace std;

enum{
    OBJ,
    AUDIO,
    TXT
};
struct object
{
    int type;
    virtual void run(){cout<<"playAction will not show this\n";}
    object() : type(OBJ){}
};
struct audiofile : public object
{
    virtual void run()  {cout<<"playing audio file\n";}
    audiofile()  {type = AUDIO;}
};
void playAction(object* obj) {
    if (obj->type==AUDIO)
        obj->run();
}
int main() {
    audiofile a, b, c;
    object d, e, f;
    vector<object*> objList = {&a,&b,&c,&d,&e,&f};
    for(object* o: objList){
        playAction(o);
    }
    return 0;
}

您可以使用许多不同的方法来实现相同的目标,另一种方法是使用typeid来检查对象类型。只需在上面的示例中将这些更改应用于playAction:

void playAction(object* obj) {
    if (typeid(*obj)==typeid(audiofile))
        obj->run();
}  

答案 4 :(得分:0)

以下似乎有效。
它需要RTTI。

template< typename DerivedT, typename ContainerT, typename MemberFunctionT,typename ...ParamTs >
void perform_on_all( ContainerT& cont, MemberFunctionT func, ParamTs ...params )
{
    for( auto&& itm : cont )
    {
        DerivedT* d = dynamic_cast<DerivedT*>(itm);
        if(d) (d->*func)(params...);
    }
}

struct Base
{
    virtual ~Base() {}
};

struct Test : public Base
{
    virtual void Foo() =0;
    virtual void Bar(int) =0;
};

void testit()
{
    vector<Base*> tvec;

    perform_on_all<Test>( tvec, &Test::Foo );
    perform_on_all<Test>( tvec, &Test::Bar, 3 );
}

答案 5 :(得分:0)

因为我有点像git,所以我喜欢重新实现RTTI。

template<class...>struct types{using type=types;};
template<class X>struct tag{using type=X;};

是一系列类型。

template<class X>
class get_interface_base {
  virtual X* get_interface( tag<X> ) { return nullptr; };
  virtual X const* get_interface( tag<X> ) const { return nullptr; };
};
template<class types>
struct interfaces {
  virtual ~interfaces() {};
  void get_interface() {
    static_assert(false,
      "use the free function I* get_interface<I>(*this) please."
    );
  }
};
template<class X, class...Xs>
struct interfaces<types<X,Xs...>:
  get_interface_base<X>,
  interfaces<types<Xs...>>
{
  using get_interface_base<X>::get_interface;
  using interfaces<types<Xs...>>::get_interface;
};

现在我们有界面演员:

namespace details {
  template<class I, class X>
  I* get_interface( X& x, std::false_type /*is_const*/ ) {
    return x.get_interface( tag<Tag>{} );
  }
  template<class I, class X>
  I const* get_interface( X const& x, std::true_type /*is_const*/ ) {
    return x.get_interface( tag<Tag>{} );
  }
}
// use: Playable* p = get_interface<Playable>(*foo);
template<class I, class X>
typename std::conditional<std::is_const<X>::value, I const, I>::type*
get_interface( X& x ) {
  using is_const = std::integral_constant<bool,
    std::is_const<X>::value || std::is_const<I>::value
  >;
  typename I2 = typename std::remove_const<I>::type;
  return details::get_interface<I2>(x, is_const{});
}
// function object version:
template<class I>
struct get_interface_t {
  template<class T>
  I* operator()(T* t)const{
    if (!t) return nullptr;
    return (*this)(*t);
  }
  template<class T>
  I* operator()(T& t)const{
    return get_interface<I>(t);
  }
};

它为我们提供了一个编译时确定的接口列表和一个虚拟表查找,以从类中获取接口。

您在某处维护了一个全局using supported_types = types< Playable, Chickenable, Deletable >;,并且您的基类公开地从interfaces<supported_types>继承。如果get_interface<Foo>不在Foo中,则supported_types尝试const在编译时失败。聪明地支持supported_types

实例支持的接口可以通过索引公开到index_of<T, types<?...>>类型列表中。我们可以编写std::vector<T*>来将所述整数与所讨论的类型进行比较。

另一方面,我们可以进行魔术转换。

std::vector<I*>template<class I> struct get_interfaces_t { template<class Ts> std::vector<I*> operator()( Ts&& ts )const{ std::vector<I*> retval; using std::begin; using std::end; std::tranform( begin(ts), end(ts), std::inserter(retval, retval.end()), get_interface_t<I>{} ); return retval; } }; 的变压器:

types<Is...>

一个神奇的开关,它采用类型列表Z一个运行时值i,一个变换器F,一个函数对象T和一个通用的东西F( Z<I>(T) ),并调用I Is...,其中falsei中的第i个类型。如果template<class types, template<class>class Z> struct magic_switch_t; template<class...Is, template<class>class Z> struct magic_switch_t<types<Is...>> { template<class T, class F> bool operator()(size_t i, T&& t, F&& f)const { typedef void(*f)( T&&, F&& ); static f arr[] = { [](T&& t, F&& f){ std::forward<F>(f)( Z<Is>{}(std::forward<T>(t)) ); }... }; if ( i >= sizeof...(Is) ) return false; arr[i]( std::forward<T>(t), std::forward<F>(f) ); return true; } }; 超出范围,则返回I*

F

声称是C ++ 11的一些编译器在上述代码中失败。

现在,我们可以查询您的对象以获取支持的接口,找到支持的接口,然后使用魔术开关来调用一个函数(例如,一个带有std::vector<I*>向量的函数)。

上面的

F应该对每个bool都有重载,即使那些不支持该操作的也是如此。他们可以什么都不做。我们可以假设std::vector<I*>返回一个struct play_stuff_t { std::true_type operator()( std::vector<Playable const*> const& targets ) const { do_play( targets ); return {}; } template<class X> std::false_type operator()( std::vector<X const*> const& ) const { return {}; } }; 返回值,如果我们选择了一些代码更改,那么会传播该错误。

如果系统支持部分应用程序(即,当2个文件中只有1个是音乐时播放),那么do_play将包含操作不适用的空指针。

用于播放的功能对象可能是:

magic_switch

其中get_interface<Blah>是一个支持播放可播放向量的自由函数。

要点:

  • 创建自己更高效的RTTI,在固定的类型列表上运行。

  • 将功能作为索引公开到上面的固定类型列表中。这些可以是索引的向量,或位域中的位,无论如何。

  • 确定元素列表的功能

  • 调用时,使用X将其转换为编译时类型,并将元素列表映射到相应接口的列表

最后请注意,虽然Is...可能看起来像一个演员,但是一个对象可以返回一个指向任何的指针 - 它自己作为该接口,作为子对象,甚至是全局某种对象。如果我们想要抛弃一些东西(允许类在该调用上返回一个临时接口对象),我们甚至可以为返回的指针添加生命周期管理。

我们还可以编写一个CRTP类,给定一个类Bs...,一组类型Bs...和一组基础get_interface,继承自X和通过将[{1}}隐式地转换为接口类型来实现get_interface_base。如果我们从每个virtual Bs...进行继承,那么这个类就更容易编写(它摆脱了传递{{1}}的要求,并使继承的线性展开变得更加容易)