2

我目前正在遭受脑放屁。我以前做过,但我不记得确切的语法,也看不到我写的代码,因为当时我在另一家公司工作。我有这样的安排:

class P
{
// stuff
};

class PW : public P
{
// more stuff
};

class PR : public P
{
// more stuff
};

class C
{
public:
    P GetP() const { return p; }    
private:
    P p;
};

// ...
    P p = c.GetP( ); // valid
    PW p = c.GetP( ); // invalid
    PR p = c.GetP( ); // invalid
// ...

现在我想让 P 与 PW 和 PR 互换(因此 PW 和 PR 可以互换)。我可能可以摆脱强制转换,但仅在这个模块中,这种代码更改就发生了很多次。我很确定它是一个操作员,但对于我的生活,我不记得是什么。

如何用最少的代码使 P 与 PW 和 PR 互换?

更新:提供更多说明。P 代表 Project,R 和 W 分别代表 Reader 和 Writer。Reader 拥有的只是加载代码 - 没有变量,而 writer 拥有用于简单写入的代码。它需要分开,因为阅读和写作部分有各种管理器类和对话框,这不是项目真正关心的项目文件的操作。

更新:我还需要能够调用 P 和 PW 的方法。因此,如果 P 有一个方法 a() 和 PW 作为方法调用 b() 那么我可以:

PW p = c.GetP();
p.a();
p.b();

基本上是为了使转换透明。

4

10 回答 10

3

你试图强制实际变量,而不是指针。要做到这一点需要演员。但是,如果您的类定义如下所示:

class C

    {
        public: 
            P* GetP() const { return p; }
        private:
            P* p;
    }

然后,无论 p 是指向 P、PW 还是 PR 的指针,您的函数都不会改变,并且在函数返回的 P* 上调用的任何(虚拟)函数都将使用 P、PW 或 PR 中的实现取决于成员 p 是什么..

我想要记住的关键是Liskov 替换原则。由于 PW 和 PR 是 P 的子类,因此可以将它们视为 Ps。但是,PW 不能被视为 PR,反之亦然。

于 2008-09-23T16:06:25.747 回答
3

如果你想让这部分编译:



// ...
    P p = c.GetP( ); // valid
    PW p = c.GetP( ); // invalid
    PR p = c.GetP( ); // invalid
// ...

您需要能够将 P 构造/转换为 PW 或 PR。你需要做这样的事情:



class PW : public P
{
    PW(const P &);
// more stuff
};

class PR : public P
{
    PR(const P &);
// more stuff
};

或者你的意思更像是:


class P
{
    operator PW() const;
    operator PR() const;
// stuff
};
于 2008-09-23T17:26:19.783 回答
2

在上面的代码中,您遇到了与切片问题相反的问题。

您要做的是将 P 分配给包含比源对象更多信息的 PW 或 PR。你怎么做到这一点?假设 P 只有 3 个成员变量,但 PW 有 12 个附加成员 - 当您编写时,这些成员的值来自哪里PW p = c.GetP()

如果这个赋值实际上有效的,这应该真的表明某种设计怪异,那么我将实现PW::operator=(const P&)and PR::operator=(const P&)PW::PW(const P&)and PR::PR(const P&)。但那天晚上我睡得不好。

于 2008-09-23T15:58:36.403 回答
1

要通过 P 使 PW 和 PR 可用,您需要使用引用(或指针)。所以你真的需要改变 C 的接口,所以它返回一个引用。

旧代码中的主要问题是您将 P 复制到 PW 或 PR 中。这是行不通的,因为 PW 和 PR 可能比 P 拥有更多的信息,并且从类型的角度来看,P 类型的对象不是 PW 或 PR。虽然 PW 和 PR 都是 P。

将代码更改为这样,它将编译:如果您想在运行时返回从 P 类派生的不同对象,那么 C 类必须能够存储您期望的所有不同类型并在运行时专门化。因此,在下面的课程中,我允许您通过传递指向将通过引用返回的对象的指针来进行专业化。为了确保对象是异常安全的,我将指针包装在智能指针中。

class C
{
    public:
        C(std::auto_ptr<P> x):
            p(x)
        {
            if (p.get() == NULL) {throw BadInit;}
        }
        // Return a reference.
        P& GetP() const { return *p; }        
    private:
        // I use auto_ptr just as an example
        // there are many different valid ways to do this.
        // Once the object is correctly initialized p is always valid.
        std::auto_ptr<P> p;
};

// ...
P&  p = c.GetP( );                   // valid
PW& p = dynamic_cast<PW>(c.GetP( )); // valid  Throws exception if not PW
PR& p = dynamic_cast<PR>(c.GetP( )); // valid  Thorws exception if not PR
// ...
于 2008-09-23T16:20:59.673 回答
1

鉴于一切都是按值传递的,这种做法是明智的。不知道是不是你想的那样。

class P
{
public:
    template <typename T>
    operator T() const
    {
        T t;
        static_cast<T&>(t) = *this;
        return t;
    }
};
于 2008-09-23T16:52:37.773 回答
0

也许您的意思是dynamic_cast运算符?

于 2008-09-23T16:01:14.010 回答
0

它们不能完全互换。PW是P。PR是P。但是P不一定是PW,也不一定是PR。您可以使用 static_cast 将指针从 PW * 转换为 P *,或从 PR * 转换为 P *。由于“切片”,您不应该使用 static_cast 将实际对象转换为它们的超类。例如。如果您将 PW 的对象投射到 P 中,则 PW 中的额外内容将被“切掉”。您也不能使用 static_cast 从 P * 转换为 PW *。如果您真的必须这样做,请使用 dynamic_cast,它将在运行时检查对象是否实际上属于正确的子类,如果不是,则给您一个运行时错误。

于 2008-09-23T16:03:10.370 回答
0

我不确定你的确切意思,但请耐心等待。

他们已经是。只需将所有内容称为 P,您就可以假装 PR 和 PW 是 P。PR 和 PW 还是有区别的。

使所有三个等价会导致Liskov 原则出现问题。但是,如果它们真的是等价的,为什么还要给它们起不同的名字呢?

于 2008-09-23T16:03:34.503 回答
0

第二个和第三个将是无效的,因为它是一个隐式向上转换——这在 C++ 中是一件危险的事情。这是因为您要转换的类比分配的类具有更多功能,因此除非您自己显式转换,否则 C++ 编译器会抛出错误(至少应该如此)。当然,这稍微简化了一些事情(您可以将 RTTI 用于某些可能与您想要安全地做的事情有关的事情,而不会引起对坏对象的愤怒)——但简单性始终是解决问题的好方法。

当然,正如在其他一些解决方案中所述,您可以解决这个问题——但我认为在您尝试解决这个问题之前,您可能需要重新考虑设计。

于 2008-09-23T16:09:26.347 回答
-1

使用指向 P 而不是对象的引用或指针:

class C
{
 public:
  P* GetP() const { return p; }
 private:
  P* p;
};

这将允许 PW* 或 PR* 绑定到 Cp 但是,如果您需要从 P 转到 PW 或 PR,则需要使用 dynamic_cast<PW*>(p),这将返回 PW* p 的版本或 p 的 NULL 不是 PW*(例如,因为它是 PR*)。不过,Dynamic_cast 有一些开销,如果可能的话最好避免(使用虚拟)。

您还可以使用 typeid() 运算符来确定对象的运行时类型,但它有几个问题,包括您必须包含以及它无法检测到额外的派生。

于 2008-09-23T16:00:17.383 回答