【问题标题】:C++ Concatenate containers of polymorphic objectsC++ 连接多态对象的容器
【发布时间】:2017-12-10 23:58:09
【问题描述】:

假设我有一个虚拟基类和一些派生的具体类:

class Base { ... }
class DerivedA : public Base { ... }
class DerivedB : public Base { ... }
class DerivedC : public Base { ... }

在某个地方我有每个派生类的对象向量:

std::vector<DerivedA> my_a;
std::vector<DerivedB> my_b;
std::vector<DerivedC> my_c;

现在,我经常需要遍历所有三个向量中的所有元素并练习基类接口。我可以编写三个 for 循环,每个循环都完全相同。但显然这远非最佳解决方案。

有没有一种聪明的方法可以将向量连接到一个带有基类指针/引用的公共容器中,这样我只需要迭代一次?或者任何其他想法如何优雅地解决这个问题?

【问题讨论】:

  • 我对此的理想选择是某种range_of(my_a, my_b, my_c) 函数,它返回一个基本类型的范围。
  • 这可能对你有用:stackoverflow.com/questions/8511035/…
  • 一个包含对向量的引用的自定义迭代器类?
  • 调用函数模板?
  • Range V3 的view::concat 似乎应该能够做到这一点,但我无法让它工作

标签: c++ c++11 polymorphism containers


【解决方案1】:

在您当前的情况下,不需要多态性。您可以简单地使用 variadic template + higher-order function 来迭代向量。这是一个使用 fold 表达式的 C++17 解决方案:

template <typename F, typename... Vectors>
void for_all_vectors(F&& f, Vectors&&... vs)
{
    (std::for_each(std::forward<Vectors>(vs).begin(), 
                   std::forward<Vectors>(vs).end(), 
                   f), ...);
}

用法:

int main()
{
    std::vector<A> my_a;
    std::vector<B> my_b;
    std::vector<C> my_c;

    for_all_vectors([](const auto& x){ something(x); }, my_a, my_b, my_c);
}

live example on wandbox


在 C++11/14 中,您可以将折叠表达式替换为 for_each_argument:

template <typename TF, typename... Ts>
void for_each_argument(TF&& f, Ts&&... xs)
{
    return (void)std::initializer_list<int>{
        (f(std::forward<Ts>(xs)), 0)...};
}

template <typename F, typename... Vectors>
void for_all_vectors(F&& f, Vectors&&... vs)
{
    for_each_argument([&f](auto&& v)
    { 
        std::for_each(v.begin(), v.end(), f);
    }, std::forward<Vectors>(vs)...);
}

live example on wandbox

我解释了这个 sn-p 背后的想法,并在这个 CppCon 2015 演讲中对其进行了扩展:"for_each_argument explained and expanded"

【讨论】:

  • @AndyG:很好,我现在正在转发折叠表达式示例中的向量。关于F&amp;&amp;:它可以接受不可移动的临时对象(例如,对不可移动的东西进行广义捕获的lambda)F不能。
  • 好的,这对我来说似乎很先进。恐怕人们在代码审查时会抱怨很多…… C++17 的解决方案看起来很漂亮,但是,我现在只能使用 C++14。我会尝试让语法更舒服,然后可能会使用这个解决方案。无论如何,非常感谢 - 现在你得到 +1,也许以后我会接受!
  • @GeorgP 我认为你的谨慎是正确的。我认为这取决于您的代码需要有多通用。您可能多久需要这种功能以及您实际拥有多少对象向量。如果您实际上只有三个向量并且只执行一次,那么可能不需要可变参数模板。
【解决方案2】:

只要有一个指向基类的指针。你不能有一个 base 类型的向量并将派生类放入其中,因为它们的大小、功能可能不同等。

所以我要做的是创建一个向量或类型 base*,然后您可以连接派生类的指针。

可能看起来像:

vector<base*> v;
v.push_back(&derivedClassVariableA);
v.push_back(&derivedClassVariableB);

那么只要您要使用的函数在基类中是虚拟的并且在派生中定义,那么您应该可以使用

【讨论】:

  • 因此,如果我理解正确,我必须编写一个方法,连续迭代每个向量,获取指向每个元素的指针,将其转换为基类指针,将指针推送到一个新向量,最后返回新向量。在这个方法之外,我会迭代新向量的元素。这也是我解决它的第一个想法,但它看起来非常手动,并且在性能方面可能也不是最好的。
  • 这取决于您需要访问的方法。如果它们相同但定义不同并且在基类中也是虚拟的,则不需要做任何特别的事情。如果没有,那么你不能这样做。例如,如果您的基类有类似“Print()”的东西,并且它是在派生类中定义的,那很好。在这种情况下,您可以将指向每个对象的指针推回基类的向量中
【解决方案3】:

一个简单的解决方案是使用一个模板函数来迭代向量的成员并调用相应的函数:

class Base {
public:
    virtual int getX() = 0;
};

class Derived1 : public Base {
public:
    int x1=1;
    virtual int getX() { return x1; };
};

class Derived2 : public Base {
public:
    int x2=2;
    virtual int getX() { return x2; };
};

template<typename T>
void callPolymorphic(std::vector<T> &v) {
    for (T& a : v) {
        cout << a.getX() << " ";
    }
}

int main() {

    std::vector<Derived1> my_1(5);
    std::vector<Derived2> my_2(5);

    callPolymorphic(my_1);
    callPolymorphic(my_2);

    return 0;
}

【讨论】:

    【解决方案4】:

    我会创建一个函数模板或一个通用 lambda 并调用它 3 次:

    auto my_loop = [](auto& vec){
      for (auto& base : vec) {
          // do something with base...
      }
    };
    my_loop(my_a);
    my_loop(my_b);
    my_loop(my_c);
    

    【讨论】:

      【解决方案5】:

      另一方面,您可以制造自己的自制视图适配器:

      #include <iostream>
      #include <vector>
      #include <functional>
      #include <iterator>
      
      struct Base {
          virtual int f() const = 0;
          virtual ~Base() {}
      };
      
      struct D1: Base {
          int f() const { return 42; }
      };
      
      struct D2: Base {
          int f() const { return 314; }
      };
      
      template<typename T, typename... Left, typename... Right>
      inline std::vector<T, Left...> operator+(std::vector<T, Left...> &&left, std::vector<T, Right...> &&right) {
          std::vector<T, Left...> retVal(std::move(left));
          using mi = std::move_iterator<typename std::vector<T, Right...>::iterator>;
          retVal.insert(retVal.end(), mi(right.begin()), mi(right.end()));
          return retVal;
      }
      
      int main() {
          std::vector<D1> v1(3);
          std::vector<D2> v2(4);
          using View = std::vector<std::reference_wrapper<const Base>>;
          View b(View(v1.cbegin(), v1.cend()) + View(v2.cbegin(), v2.cend()));
          for(Base const &item: b) std::cout << item.f() << std::endl;
      }
      

      (请注意,底层查看的容器可以是任何容器,vectors 可以,但它们的元素类型应该是一致的。)

      【讨论】:

      • 可以肯定,这是一个聪明的解决方案,但我不喜欢我们需要对所有项目进行两次迭代,以及为引用包装器分配额外的存储空间。
      • @AndyG 当然,但请注意:1) ref wrapper 不是一个巨大的对象; 2)您可能需要对其进行两次以上的迭代,甚至更多,在这种情况下,第一次迭代并没有太大的区别; 3) 我想知道,如果我们真的最多迭代一次,是否有编译器/STL 实现组合来优化容器的构造。
      • 这是一个好点,只要一个人的用例是重读的。对于每次写入,您都必须重构 View(尽管您可以优化它,只要顺序不重要)。
      • 嗯...你的意思是重建相同的视图?但为什么?为什么不保留它?
      • 如果工作流可以迭代过时的信息,那么保持它很好。我只是建议在插入v1v2 之后,最终用户可能希望对它们进行下一次迭代以合并新元素。否则,如果顺序无关紧要,我们需要重建 View 或将另一个 reference_wrapper&lt;const Base&gt; 附加到 b
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-30
      • 1970-01-01
      • 1970-01-01
      • 2020-06-30
      相关资源
      最近更新 更多