基于范围的C++11 for循环用于派生对象。

6
如果我有一个指向父类的指针向量,并且这个向量是通过实例化从父类派生的对象来初始化的,那么似乎我不能使用范围 for 循环以便获取导出对象作为元素。以下是一个简单的示例:
#include <vector>

class Parent {};
class Derived : public Parent {};

int main()
{
    std::vector<Parent*> objects { new Derived(), new Derived() };

    for (Derived* d : objects) {    // ERROR
        // Use d
    }

    return 0;
}

有没有一种简洁的方式可以做到我想要的(即,遍历派生对象)?我知道可以像这样做:

for (Parent* p : objects) {
    Derived* d = static_cast<Derived*>(p);
    // Use d
}

但是在C++11中,这是最干净的方法吗?还有其他替代方案吗?

16
如果你确信所有指针都是 Derived*,为什么一开始不使用 vector<Derived*> 呢?如果你不确定,那么你的转换有什么用处(无论是显式转换还是在循环中以某种神奇的方式隐式转换)? - Igor Tandetnik
1
我认为没有其他方法可以完成你所做的事情。向下转型是危险的,因此您必须明确地执行它,以告诉编译器保持安静。 - Siyuan Ren
1
实际上这样做没有意义,如果你有一个基础集合,你需要自己解决向下转型和测试。 - Casper Beyer
2
你真正应该做的是在父类中定义一个虚函数,在循环中调用它。 - Neil Kirk
Igor,我的情况并不简单。指向父对象的容器是抽象类(称为Model)的成员。从Model派生的类(例如Submodel1和Submodel2)使用特定(派生)对象初始化容器。在Model中,我不知道具体的派生对象是什么,所以我将它们视为Parent*。但在子模型中,我知道它们是什么,这就是为什么我要对它们进行下转换的原因。 - redcurry
3个回答

3

你可以使用 dynamic_cast 来实现这一点:

class Parent {
public:
    virtual ~Parent() { } // make class polymorphic
};
class Derived : public Parent {};

int main()
{
    std::vector<Parent*> objects { new Derived(), new Derived() };

    for (Parent* d : objects) {    // ERROR
        auto temp = dynamic_cast<Derived*>(d);
        if (d) // cast succeeded
        {
            // we found the right class
        }
    }

3

范围迭代器在语法上更加简洁,而且你无法在for循环内进行类型转换。

for (Parent* p : objects) {
    Derived* d = static_cast<Derived*>(p);
    // Use d
}

如果你确实需要向下转型,应该在函数体内进行。一个设计良好的程序并不需要真正的向下转型。请注意!PS:在向下转型时,请优先使用动态转型而非静态转型。但是当您使用dynamic_cast时,会有一些RTTI成本。请查看 - Performance of dynamic_cast?

1
实际上应该使用 static_pointer_cast<Parent>(p),因为他应该使用 shared_ptr<Parent> 来持有内存。 - David G
2
@0x499602D2: 在他的例子中更像是std::unique_ptrstd::shared_ptr有点过头了。 - Deduplicator
@Deduplicator 我曾经考虑过这个问题,但是我又想到如果有一个重载的 static_pointer_cast 可以接受一个 unique_ptr,但出于某种原因似乎并没有。 - David G
一个设计良好的程序实际上不需要向下转型。请问我可以在哪里学习如何设计这样的程序呢? - sergiol

3
cppreference.com页面指出,基于范围的for循环表达式产生类似以下代码的代码(__range__begin__end仅用于说明):

for (range_declaration : range_expression) loop_statement

{
    auto && __range = range_expression ; 
    for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) { 
        range_declaration = *__begin; 
        loop_statement 
    } 
}

根据该描述,我推断出__begin__end必须是同一类型,但实际情况并非如此。指向Parent的指针不是指向Derived的指针,它们必须显式转换,而基于范围的for循环根本不执行任何转换。
为了实现您想要的效果,我会创建一个帮助类来执行转换1,例如:
struct Helper
{
    Helper(Parent *p) : obj(p) {}
    Parent *const obj;
    operator Derived *() { return static_cast<Derived *>(obj); }
};

这个帮助器允许我们做以下事情:

std::vector<Parent*> objects { new Derived(), new Derived() };

for (Helper h : objects) {
    Derived *d = h;
    d->use();
}

我们可以将助手类模板化,以便在其他父派生上下文中使用它:
template <typename T> struct THelper
{
    THelper(Parent *p) : obj(p) {}
    Parent *const obj;
    operator T *() { return static_cast<T *>(obj); }
};

...
...

for (THelper<Derived> h : objects) {
    Derived *d = h;
    d->use();
}

for (THelper<Derived2> h : objects) {
    Derived *d = h;
    d->use();
}

但是,如果你的objects向量包含混合类型,则可能会很危险。

作为改进,你可以用dynamic_cast替换static_cast并检查nullptr,但你必须注意它的开销

这不是我在我的项目中使用的解决方案,但我希望它看起来对你很干净整洁;)

编辑

然而,转换(无论是使用static_cast还是通过Helper类)仍然需要作为for循环内的额外一行。

正如我们之前讨论的那样,强制转换(静态或动态)是必需的,但如果你担心添加额外一行会使循环的整洁性受到截断,重载operator ->应该就可以解决问题:

struct Helper
{
    Helper(Parent *p) : obj(p) {}
    Parent *const obj;
    operator Derived *() { return static_cast<Derived *>(obj); }
    Derived *operator ->() { return static_cast<Derived *>(obj); }
};

for (Helper h : objects) {
    h->use();
}

请注意,如果您正在使用 operator ->,则无法检查 nullptr,如果您的 objects 向量具有混合派生类型,则这是必要的。
我已更新 演示实例 以包含新代码。
虽然我们可以从评论中了解到更好的实现目标的方法,但是这种方法也是可行的。

我很感谢你的解决方案;然而,在for循环内部仍需要进行转换(无论是使用static_cast显式转换还是通过Helper类)。从人们的答案和评论,包括你自己的,似乎范围-based for循环不能进行任何转换。 - redcurry
我接受这个答案,因为你在帖子的第一部分明确回答了我的问题。 - redcurry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接