多态类成员变量

15

我有一个名为messenger的类,它依赖于一个printer实例。 printer是一个多态基类,实际对象在构造函数中传递给messenger

对于非多态对象,我只需执行以下操作:

class messenger {
public:
    messenger(printer const& pp) : pp(pp) { }

    void signal(std::string const& msg) {
        pp.write(msg);
    }

private:
    printer pp;
};

但是当printer是一个多态基类时,这种方法就不再适用了(切片)。

考虑到以下情况,什么是使其正常工作的最佳方式:

  1. 我不想在构造函数中传递指针。
  2. printer类不应该需要一个虚拟的clone方法(=需要依赖于复制构造)。

我不想在构造函数中传递指针,因为API的其他部分使用实际对象而不是指针,并且在这里使用指针作为参数会让人混淆/不一致。

在C++0x下,我可以使用unique_ptr和模板构造函数:

struct printer {
    virtual void write(std::string const&) const = 0;
    virtual ~printer() { } // Not actually necessary …
};

struct console_printer : public printer {
    void write(std::string const& msg) const {
        std::cout << msg << std::endl;
    }
};

class messenger {
public:
    template <typename TPrinter>
    messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }

    void signal(std::string const& msg) {
        pp->write(msg);
    }

private:
    std::unique_ptr<printer> pp;
};

int main() {
    messenger m((console_printer())); // Extra parens to prevent MVP.

    m.signal("Hello");
}

这是最好的替代方案吗?如果是,那在0x之前什么是最好的方法?并且有没有办法在构造函数中摆脱完全不必要的复制?不幸的是,在这里移动临时对象是行不通的(对吗?)。


嗯,unique_ptr 在实际使用中表现得就像指针一样,唯一的区别在于生命周期管理。 - Jan Hudec
1
+1 用额外的括号来防止MVP。 :D - Nawaz
顺便说一下,那个构造函数模板可能会干扰你的拷贝构造函数,至少我记得 Visual Studio 在某个时候会抱怨它... - Nim
你能解释一下“额外的括号来防止 MVP”的含义吗? - Mark Ingram
1
@Mark MVP = most vexing parse@Mark MVP = 最令人烦恼的解析 - Konrad Rudolph
显示剩余7条评论
6个回答

9
没有虚拟克隆方法就无法克隆多态对象。因此,您可以选择以下方式之一:
  • 传递并持有引用,并确保在构造信使的代码中打印机未被销毁。
  • 传递并持有智能指针,并使用new创建打印机实例。
  • 通过克隆方法传递引用并在堆上创建打印机实例。
  • 将引用传递给模板的实际类型,在仍然知道类型的情况下使用new创建实例。
最后一种是您使用C++0x std::unique_ptr建议的方法,但在这种情况下,C++03 std::auto_ptr也会为您提供完全相同的服务(即您不需要移动它们,它们是相同的)。
编辑:好的,额外再提供一种方式:
  • 将打印机本身作为实际实现的智能指针。这样做可以同时实现可复制和多态,但代价是有些复杂。

哈哈,我完全忘记了 auto_ptr - Konrad Rudolph

2
Unfortunately, moving the temporary doesn’t work here (right?).

错误。直言不讳地说,这正是右值引用的用途。一个简单的重载可以快速解决手头的问题。

class messenger {
public:
    template <typename TPrinter>
    messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }
    template <typename TPrinter>
    messenger(TPrinter&& pp) : pp(new TPrinter(std::move(pp))) { }

    void signal(std::string const& msg) {
        pp->write(msg);
    }

private:
    std::unique_ptr<printer> pp;
};

在C++03中,相同的概念也适用,但是将auto_ptr替换为unique_ptr并且放弃右值引用重载。

此外,如果您可以接受一些不太正规的接口,可以考虑使用某种“虚拟”构造函数。

class messenger {
public:
    template <typename TPrinter>
    messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }
    template<typename TPrinter> messenger(const TPrinter& ref, int dummy) 
        : pp(new TPrinter()) 
    {
    }
    void signal(std::string const& msg) {
        pp->write(msg);
    }

private:
    std::unique_ptr<printer> pp;
};

或者你可以考虑与C++03中 "移动" 使用的auto_ptr 相同的策略。当然要小心使用,但是完全合法和可行的。问题在于这会影响所有的printer子类。


2
将评论扩展为适当的回答...
主要问题在于所有权。从您的代码中,似乎每个 messenger 实例都拥有自己的 printer 实例 - 但实际上,您正在传递一个预先构造的打印机(可能带有一些附加状态),然后需要将其复制到自己的 printer 实例中。鉴于对象 printer 的暗示性质(即打印某些东西),我认为它所打印的内容是共享资源 - 在这种情况下,每个 messenger 实例拥有自己的 printer 副本是没有意义的(例如,如果您需要锁定访问 std::cout 怎么办)?
从设计角度来看,messenger 在构建时实际上需要的是指向某些共享资源的指针 - 在这种情况下,shared_ptr(最好是 weak_ptr)是更好的选择。
现在,如果您不想使用 weak_ptr,而是想存储引用,请考虑是否可以将 messengerprinter 的类型耦合,耦合留给用户处理,您不关心 - 当然,这样做的主要缺点是 messenger 将无法包含。注意:您可以指定一个 traits(或策略)类,messenger 可以在其上输入类型,并为打印机提供类型信息(可以由用户控制)。
第三个选择是,如果您完全控制打印机集合,则持有变体类型 - 在我看来,这更加清洁,并避免了多态性。
最后,如果您无法耦合,无法控制打印机,并且想要自己的 printer 实例(相同类型),则转换构造函数模板是前进的方法,但添加一个 disable_if 以防止其被错误调用(即作为普通复制 ctor)。
总之,我会将打印机视为共享资源,并保存 weak_ptr,因为它可以更好地控制该 共享资源

不要过于纠结于示例标识符的语义,因为很难找到一个简单的示例来解释问题。但在我的实际用例中,该对象并不是共享资源。 - Konrad Rudolph
@Nim 哪个复制构造函数的要求?不,根本没有共享资源。 - Konrad Rudolph
@Konrad,即使在你的简单示例中,你也在调用printer的复制构造函数(对我来说,这意味着你正在从传递给messenger拥有的printer实例中复制一些状态)。至于智能指针的构造,除非我漏掉了非常明显的东西...(这很可能是情况),否则你不可以只是做new TPrinter()吗? - Nim
@Nim 在我的简单示例中,如果不调用构造函数,有什么替代方法呢?毕竟,我正在“传递一个对象”。在我的第二个示例中也是如此。仅仅因为我的对象没有共享状态,并不意味着它们没有任何状态 - 它们确实有状态。 - Konrad Rudolph
@Konrad,我明白了,所以你通过复制构造函数传递了可能的初始状态。好的,没问题,你比我更了解你的情况! :) 我想我唯一要说的就是你现在的代码可以工作,只需添加disable_if以消除歧义即可。 - Nim
显示剩余2条评论

1

为什么你不想传递指针或智能指针呢?

无论如何,如果你总是在构造函数中初始化打印机成员,你可以使用引用成员。

private:
    printer& pp;
};

并在构造函数初始化列表中进行初始化。


这将导致悬空引用,因为一旦 m 离开作用域就会出现问题。至于为什么我不想使用智能指针 - 因为它使 API 不一致(用户何时需要使用对象,何时需要使用(智能)指针?)并泄漏实现细节。 - Konrad Rudolph
无论你是否需要打印机对象的副本,都可以。如果你不需要副本,除了引用和指针之外,你几乎没有什么可做的了。我也不明白为什么使用智能指针会泄漏实现细节。 - Assaf Lavie
@gigantt.com 它泄露了实现细节,因为用户不关心类在内部如何处理对象(甚至可能会发生变化)。它所关心的只是该类需要这样一个对象的实例。无论这是短暂的(因此只需要引用)还是存储的都不应该有影响。但我开始意识到,资源所有权将始终泄漏信息,无论如何。 - Konrad Rudolph
这个所谓的泄漏有什么重要性吗?用户不知道这个类是否实际上存储了一个指针。它只知道必须提供一个多态项,在C++中,这是通过指针/引用来实现的...我看不出有什么问题。 - Assaf Lavie
@gigantt 再次强调,用户并不关心指针或引用。用户甚至不关心多态性。从他的角度来看,一个对象被传递到构造函数中。而且多态性也不需要指针,引用同样可以正常工作 - 但前提是您不会接管对象的所有权。 - Konrad Rudolph
@Konrad:“用户甚至不关心多态性” - 我似乎不同意。服务器接受接口,用户可以自由提供该接口的任何自定义实现 - 这是合同的一部分,所以用户非常清楚。至于ref vs. ptr,我认为(正如你所说)这是一种信号,用于表明服务器类采用的所有权模型。这也必须向用户明确说明,因此它也是合同的一部分。 - davka

1

当你有一把金色的锤子时,所有东西看起来都像钉子

嗯,我的最新金色工具是类型擦除。说真的,我不会使用它,但再一次,我会传递一个指针,并让调用者创建和注入依赖项。

struct printer_iface {
   virtual void print( text const & ) = 0;
};

class printer_erasure {
   std::shared_ptr<printer_iface> printer;
public:
   template <typename PrinterT>
   printer_erasure( PrinterT p ) : printer( new PrinterT(p) ) {}

   void print( text const & t ) {
      printer->print( t );
   }
};

class messenger {
   printer_erasure printer;
public:
   messenger( printer_erasure p ) : printer(p) {}
...
};

好的,可以说这个和使用模板提供的解决方案是完全一样的,唯一的微小区别在于类型擦除的复杂性被“移动”到了类的外部。 messenger 类有自己的职责,而类型擦除不是其中之一,它可以被委派。


0

class messanger 进行模板化怎么样?

template <typename TPrinter>
class messenger {
public:
    messenger(TPrinter const& obj) : pp(obj) { }
    static void signal(printer &pp, std::string const& msg) //<-- static
    {
        pp->write(msg);
    }
private:
    TPrinter pp;  // data type should be template
};

请注意,signal()已被设置为static。这是为了利用class printervirtual能力,并避免生成新的signal()副本。您需要做的唯一努力就是像这样调用函数:
signal(this->pp, "abc");

假设您有其他数据类型不是与模板类型相关的,则可以将它们移动到非模板基类中,并且该基类可以被 messenger 继承。我没有详细描述,但希望重点更清晰。

在我的情况下不可能。实际上,如果那是可能的,整个问题就不会出现。我也不明白将signal设置为静态的有什么用处:如果我已经使用模板,我就不再关心虚函数和动态分派了。 - Konrad Rudolph
@Konrad,拥有static signal()是为了不生成许多signal的副本,因为它不会依赖于template参数。此外,在signal()内部使用pp需要传递它。然而,由于无法使用template,这种方法没有用处。 - iammilind

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