问题 C ++不鼓励收集基类 - 无论如何要伪造它?


没有类似的(Java)概念 用C ++收集

我能理解其中的原因,但我想知道是否有办法解决  它很优雅。

我已经实现了很多自定义 Collection秒。
他们都有 Iterator 这是正常的,类似于 std::vectorstd::unordered_set

他们是 MyArray<T> , MyBinaryTree<T> 和 MySet<T>

在这里,我将展示一个工作代码,显示我想要伪造它的位置。

假设我有两个级别的程序:库和用户。
它只做一件事 - User 命令 Library 吃所有 Orange*在一个桶里。

Library.h

class Library{
    public: static void eatAll(const MyArray<Orange*>& bucket);
};

Library.cpp

#include "Orange.h"
void Library::eatAll(const MyArray<Orange*>& bucket){
    for(auto orange:bucket){
        orange->eaten();
    }
}

User.h

MyArray<Orange*> bucket;
Library::eatAll(bucket);    

没关系。

现在,我想要 Library::eatAll 也支持 MyBinaryTree<Orange*>,我有一些不太理想的方法如下。

我的解决方案不好

1. Java方式

  • 使 MyBinaryTree<T> 和 MyArray<Orange*> (和它们的迭代器)继承自一个新类 Collection<T> (和 CollectionIterator<T>)。
  • 将签名更改为 Library::eatAll(const Collection<T>&)

坏处 : 一些函数的“虚拟”性能损失 Collection<T>

2.模板v1

//Library.h
template<class T> void eatAll(const T&t ){
    for(auto orange : t){
        orange->eaten();
    }
}
  • 使 eatAll 模板功能

坏处 :     执行 eatAll 必须在标题中。
我不得不 #include orange.h 在 Library.h
有时候,我真的很想转发声明。

3.模板v2

//Library.h
template<class T> void eatAll(const T&t ){
    for(auto orange : t){
        eatAnOrange(orange)
    }
}
private: void eatAnOrange(Orange* orange){
    //below implementation is inside Library.cpp
    orange->eaten();
}
  • 创造一个中间人的功能 eatAnOrange 

坏处 : 

  • 代码不太可读,不简洁,导致一点可维护性问题。
  • 如果还有很多其他功能,例如 squeezeAll(),我可能要创建很多中间人函数,例如 squeezeAnOrange()

4.运营商=()

通过隐式构造函数在3个集合类中创建转换器。
坏处 :   从创建新的集合实例中获得性能。

//Here is what it will do, internally (roughly speaking)
MyBinaryTree<Orange*> bucket;
Library::eatAll(MyArray<Orange*>(bucket)); 

我相信我的解决方案不够优雅。
有没有没有上述劣势的解决方案?

编辑:
目前的答案都比我的方法更优雅(谢谢!),但仍然有缺点: -
- Oliv的要求 #include "orange.h" 在 User.h
- 理查德·霍奇斯的虚拟函数调用。


9487
2018-03-09 07:32


起源

您可以键入擦除集合并具有接受公共基类的非模板函数,但这将引入虚拟调用(这就是Java所具有的,作为基类的接口)。否则使你的函数成为一个函数模板 T 是容器,而不是包含的类型。 - skypjack
@skypjack感谢替代解决方案,skypack! ... T 容器=我的“模板方式1”? - javaLover
该 容器概念 最接近你的想法。 - R Sahu
@R Sahu感谢,新的“概念”涵盖了这么一个非常广阔的领域!您是否在实践中使用Concept(C ++实验性功能),即已经亲自采用它? - javaLover
你应该记住的模型是,虽然java很重要 运行时多态性 (通过虚函数实现),C ++很重要 编译时多态 (通过模板实现)。 - Hurkyl


答案:


在C ++中,使用迭代器设计模式遍历集合。整个STL是围绕这个概念设计的。它可能适合您的需求:

你可以定义 eatAll 作为接受两个迭代器的函数:

template<class Iterator,class Sentinel>
void eatAll(Iterator it, Sentinel s){
    for (;it!=s;++it)
      it->eaten();
}

或者 范围就像 算法接口:

template<class Range>
void eatAll(Range& r){
    for (auto& v:r)
      v.eaten();
}

您必须将二叉树定义为范围(它必须实现 begin() 和 end())。希望树木是一种可以线性化的图形。然后,所有智能工作都将进入迭代器实现!


7
2018-03-09 08:21



橙色类型和集合类型都隐藏在模板/自动...很好的技巧....我会品尝它。谢谢!唯一的缺点是IDE会头晕,所以上下文线索(ctrl +函数内的空格)对橙色不起作用。 - javaLover
@javaLover这个设计模式在STL中到处都是,我读过的大多数C ++程序。你可以对它充满信心。 - Oliv
哦,我刚刚发现另一个缺点。 ......现在,User.h 将不得不 #include "orange.h",如果我不包括它 Library.h。 ....对吗? ....这有点难以管理+较慢的编译时间。 ...是的,你的解决方案仍然比我的好 模板v1 / v2 做法。 - javaLover
这是模板元编程的问题。编译器现在非常快(至少如果你并行编译的话) make -j8),你不应该担心这个问题来自于时代......(2010年之前)。如果这是我的解决方案,我会感到非常自豪,但这个解决方案是在我出生之前发明的!数十年来,它被数以百万计的程序员和架构师用于C ++,它的好处通过实验证明了!你应该采纳它。 - Oliv


如果你想要它真正的多态,那么我们必须处理两件事:

  1. 容器的实际类型

  2. 解除引用映射的结果是包含键和值引用的对的事实。

我的观点是,对此的答案不是从容器派生,这是限制,而是创建一个多态“值迭代器”,它模拟所有迭代器并正确提取它们的值。

然后我们可以编写如下代码:

int main()
{

    std::vector<Orange> vo {
            Orange(), Orange()
    };

    std::map<int, Orange> mio {
            { 1, Orange() },
            { 2, Orange() },
            { 3, Orange() }
    };

    std::cout << "vector:\n";
    auto first = makePolymorphicValueIterator(vo.begin());
    auto last = makePolymorphicValueIterator(vo.end());
    do_orange_things(first, last);

    std::cout << "\nmap:\n";
    first = makePolymorphicValueIterator(mio.begin());
    last = makePolymorphicValueIterator(mio.end());
    do_orange_things(first, last);
}

要得到这个:

vector:
Orange
Orange

map:
Orange
Orange
Orange

这是一个最小的,完整的实现:

#include <typeinfo>
#include <memory>
#include <iostream>
#include <vector>
#include <map>
#include <iterator>

// define an orange
struct Orange {
};

// a meta-function to get the type of the value of some iterated value_type    
template<class ValueType> struct type_of_value
{
    using type = ValueType;
};

// specialise it for maps and unordered maps
template<class K, class V> struct type_of_value<std::pair<K, V>>
{
    using type = V;
};

template<class ValueType> using type_of_value_t = typename type_of_value<ValueType>::type;

// function to extract a value from an instance of a value_type    
template<class ValueType> struct value_extractor
{
    template<class V>
    auto& operator()(V&& v) const {
        return v;
    }
};

// specialised for maps    
template<class K, class V> struct value_extractor<std::pair<K, V>>
{
    template<class Arg>
    auto& operator()(Arg&& v) const {
        return std::get<1>(v);
    }
};

template<class Iter>
auto extract_value(Iter const& iter) ->  auto&
{
    using value_type = typename std::iterator_traits<Iter>::value_type;
    auto e = value_extractor<value_type> {};
    return e(*iter);
}

// a polymorphic (forward only at the moment) iterator
// which delivers the value (in the case of maps) or the element (every other container)
template<class ValueType>
struct PolymorphicValueIterator {

    using value_type = type_of_value_t<ValueType>;

private:
    struct iterator_details {
        std::type_info const &type;
        void *address;
    };

    struct concept {

        virtual std::unique_ptr<concept> clone() const = 0;

        virtual value_type& invoke_deref() const = 0;

        virtual void invoke_next(std::size_t distance = 1) = 0;

        virtual iterator_details get_details() = 0;

        virtual bool is_equal(const iterator_details &other) const = 0;

        virtual ~concept() = default;

    };

    template<class Iter>
    struct model final : concept {

        model(Iter iter)
                : iter_(iter)
        {}

        std::unique_ptr<concept> clone() const override
        {
            return std::make_unique<model>(iter_);
        }


        virtual value_type& invoke_deref() const override {
            return extract_value(iter_);
        }

        void invoke_next(std::size_t distance = 1) override
        {
            iter_ = std::next(iter_, distance);
        }

        iterator_details get_details() override {
            return {
                    typeid(Iter),
                    std::addressof(iter_)
            };
        }

        bool is_equal(const iterator_details &other) const override {
            if (typeid(Iter) != other.type) {
                return false;
            }
            auto pother = reinterpret_cast<Iter const*>(other.address);
            Iter const& iother = *pother;
            return iter_ == iother;
        }

        Iter iter_;
    };


    std::unique_ptr<concept> concept_ptr_;

public:
    bool operator==(PolymorphicValueIterator const &r) const {
        return concept_ptr_->is_equal(r.concept_ptr_->get_details());
    }

    bool operator!=(PolymorphicValueIterator const &r) const {
        return not concept_ptr_->is_equal(r.concept_ptr_->get_details());
    }

    PolymorphicValueIterator &operator++() {
        concept_ptr_->invoke_next(1);
        return *this;
    }

    value_type& operator*() const {
        return concept_ptr_->invoke_deref();
    }

    template<class Iter>
    PolymorphicValueIterator(Iter iter)
    {
        concept_ptr_ = std::make_unique<model<Iter>>(iter);
    }

    PolymorphicValueIterator(PolymorphicValueIterator const& r)
            : concept_ptr_(r.concept_ptr_->clone())
    {}

    PolymorphicValueIterator& operator=(PolymorphicValueIterator const& r)
    {
        concept_ptr_ = r.concept_ptr_->clone();
        return *this;
    }

};

template<class Iter>
auto makePolymorphicValueIterator(Iter iter)
{
    using iter_value_type = typename std::iterator_traits<Iter>::value_type;
    using value_type = type_of_value_t<iter_value_type>;
    return PolymorphicValueIterator<value_type>(iter);
}

// a test
void do_orange_things(PolymorphicValueIterator<Orange> first, PolymorphicValueIterator<Orange> last)
{
    while(first != last) {
        std::cout << "Orange\n";
        ++first;
    }
}

int main()
{

    std::vector<Orange> vo {
            Orange(), Orange()
    };

    std::map<int, Orange> mio {
            { 1, Orange() },
            { 2, Orange() },
            { 3, Orange() }
    };

    std::cout << "vector:\n";
    auto first = makePolymorphicValueIterator(vo.begin());
    auto last = makePolymorphicValueIterator(vo.end());
    do_orange_things(first, last);

    std::cout << "\nmap:\n";
    first = makePolymorphicValueIterator(mio.begin());
    last = makePolymorphicValueIterator(mio.end());
    do_orange_things(first, last);
}

2
2018-03-09 08:39



在这种方法中,我将遭遇“虚拟” - 与我的第一种方法(Java方式)相同,对吧? ....(感谢花时间写完整个代码。) - javaLover
@javaLover任何多态都会遭受(通常是想象的)“虚拟成本”。另一个答案提供了通用编程解决方案,在大多数情况下,它将更有效,但不提供多态性。付你的钱,选择你的选择...... :) - Richard Hodges
@javaLover无论你采取哪种方法,请注意两个答案(强烈地)表明不需要派生一般的“容器”。如果你想这样做,那么我会建议一个“容器视图”,它存储对容器的引用。无论你采用哪种方式,你仍然可以对迭代器进行虚拟调用。只有在每次迭代中使用非常少的逻辑进行紧密循环迭代数百万次时,这才有意义。 - Richard Hodges
@javaLover模板元编程和多态就像是同一枚硬币的两面。使用元编程,您将获得最快的程序,而它们不是同一模板的两个不同实例。但在某些方面,你会得到所谓的 代码臃肿。此时,类似于本答案中提出的类型擦除技术变得有用。 - Oliv
@javaLover如果您对C ++技术的本体感兴趣, PolymorphicValueIterator 实现称为“值类型多态”的技术,这是一种“类型擦除”。 - Oliv