代码之家  ›  专栏  ›  技术社区  ›  Patrick

C++与迭代器结合的接口编程。如何保持这个简单?

  •  9
  • Patrick  · 技术社区  · 14 年前

    在我的开发中,我正慢慢地从面向对象的方法转向基于接口的编程方法。更确切地说:

    • 过去,如果我能在课堂上分组逻辑,我已经很满意了。
    • 现在,我倾向于在接口后面放置更多的逻辑,并让工厂创建实现。

    一个简单的例子阐明了这一点。

    过去我写过这些课程:

    • 图书馆

    现在我写这些课程:

    • 髂骨的
    • 图书馆
    • 图书馆厂
    • iBook
    • 书店

    这种方法允许我为每个接口轻松实现模拟类,并在旧的、较慢的实现和新的、较快的实现之间切换,并在同一个应用程序中对它们进行比较。

    在大多数情况下,这是非常有效的,但是如果我想使用迭代器循环集合,这就成了一个问题。

    假设我的图书馆有一系列的书,我想遍历它们。在过去,这不是问题:library::begin()和library::end()返回了一个迭代器(library::迭代器),我可以很容易地在该迭代器上编写循环,如下所示:

    for (Library::iterator it=myLibrary.begin();it!=mylibrary.end();++it) ...
    

    问题在于,在基于接口的方法中,不能保证iLibrary的不同实现使用相同类型的迭代器。如果oldlibrary和newlibrary都继承自ilibrary,则:

    • OldLibrary可以使用std::vector来存储其图书,并在其开始和结束方法中返回std::vector::const_迭代器。
    • newlibrary可以使用std::list来存储它的书,并在其begin和end方法中返回std::list::const_迭代器。

    要求两个iLibrary实现返回相同类型的迭代器也不是解决方案,因为实际上增量操作(++it)需要在两个实现中以不同的方式实现。

    这意味着在实践中,我必须使迭代器也是一个接口,这意味着应用程序不能将迭代器放在堆栈上(典型的C++切片问题)。

    我可以通过将迭代器接口包装在一个非接口类中来解决这个问题,但对于我试图得到的问题,这似乎是一个相当复杂的解决方案。

    有更好的方法来处理这个问题吗?

    编辑: 在马丁的评论之后,有一些澄清。

    假设我有一个类,它返回所有按流行程度排序的图书:图书馆图书查找器。 它有begin()和end()方法返回一个引用书籍的librarybookfinder::const_迭代器。

    为了用全新的实现替换旧的实现,我想将旧的librarybookfinder放在接口ilibrarybookfinder后面,并将旧的实现重命名为oldslowlibrarybookfinder。

    然后,我的新的(快速的)实现veryFastCachingLibraryBookFinder可以从ILibraryBookFinder继承。这就是迭代器问题的来源。

    下一步可能是将界面隐藏在工厂后面,在那里我可以要求工厂“给我一个‘查找工具’,它非常擅长根据流行程度、标题或作者等返回书籍。最终得到的代码如下:

    ILibraryBookFinder *myFinder = LibraryBookFinderFactory (FINDER_POPULARITY);
    for (ILibraryBookFinder::const_iterator it=myFinder->begin();it!=myFinder.end();++it) ...
    

    或者如果我想使用其他标准:

    ILibraryBookFinder *myFinder = LibraryBookFinderFactory (FINDER_AUTHOR);
    for (ILibraryBookFinder::const_iterator it=myFinder->begin();it!=myFinder.end();++it) ...
    

    libraryBookFinderFactory的参数可以由外部因素决定:配置设置、命令行选项、对话框中的选择…每种实现都有自己的优化(例如,一本书的作者不会改变,所以这可以是一个非常静态的缓存;流行程度可以每天改变,这可能意味着一个完全不同的数据结构)。

    3 回复  |  直到 14 年前
        1
  •  3
  •   Loki Astari    14 年前

    你在这里混用隐喻。

    如果库是一个容器,那么它需要自己的迭代器,它不能重用成员的迭代器。因此,您可以将成员迭代器包装在ILibrary迭代器的实现中。

    但严格来说,图书馆不是一个容器,而是一个图书馆。
    因此,库中的方法是您可以在库中执行的操作(这里是think verbs)。库可以包含一个容器,但严格来说,它不是一个容器,因此不应该公开begin()和end()。

    因此,如果要对书籍执行操作,应要求库执行该操作(通过提供函数)。类的概念是它是自包含的。用户不应该使用getter来获取关于对象的信息,然后将其放回对象应该知道如何对其本身执行操作(这就是为什么我讨厌getter/setter破坏封装的原因)。

    class ILibrary
    {
        public:
             IBook const& getBook(Index i) const;
    
             template<R,A>
             R checkBooks(A const& librarianAction);
    };
    
        2
  •  3
  •   CashCow    14 年前

    如果你的库中有很多书,你应该考虑将你的“聚合”函数放入你的集合中,并传递希望它执行的操作。

    具有以下性质的东西:

    class ILibrary
    {
    public:
      virtual ~Ilibrary();
      virtual void for_each( boost::function1<void, IBook> func ) = 0;
    };
    
    LibraryImpl::for_each( boost::function1<void, IBook> func )
    {
        std::for_each( myImplCollection.begin(), myImplCollection.end(), func );
    }
    

    虽然可能不完全是这样,因为你可能需要处理使用共享的指针,常数等。

        3
  •  1
  •   Albert    14 年前

    为此目的(或者通常在我大量使用接口的实现中),我还为迭代器创建了一个接口,其他对象返回这个接口。它变得相当像Java。

    如果您关心在堆栈的大多数情况下使用迭代器:您的问题当然是您在编译时并不真正知道迭代器的大小,因此您无法分配正确大小的堆栈变量。但是如果您真的很关心这个问题:也许您可以编写一些包装器,它要么在堆栈上分配一个特定的大小(例如128字节),如果新的迭代器适合,它就会将其移动到那里(请确保迭代器有一个适当的接口以干净的方式允许这样做)。或者你可以用 alloca() . 例如,迭代器接口可以是:

    struct IIterator {
       // iterator stuff here
       // ---
       // now for the handling on the stack
       virtual size_t size() = 0; // must return own size
       virtual void copyTo(IIterator* pt) = 0;
    };
    

    你的包装:

    struct IteratorWrapper {
       IIterator* pt;
       IteratorWrapper(IIterator* i) {
           pt = alloca(i->size());
           i->copyTo(pt);
       }
       // ...
    };
    

    左右。


    另一种方法,如果理论上它在编译时总是清晰的(不确定这是否适用于您;这是一个明确的限制):在任何地方使用函数。这还有许多其他的缺点(主要是头文件中有所有真正的代码),但是您将拥有非常快的代码。例子:

    template<typename T>
    do_sth_with_library(T& library) {
       for(typename T::iterator i = library.begin(); i != library.end(); ++i)
          // ...
    }
    

    但是如果你过于依赖这个代码,代码可能会变得非常难看。


    另一个不错的解决方案(使代码更实用——实现 for_each 接口)由CashCow提供。

    使用当前C++,这可能会使代码使用起来有点复杂/难看。随着即将到来的C++ 0x和lambda函数,这个解决方案可以变得更加干净。