【问题标题】:Polymorphic class member variable多态类成员变量
【发布时间】:2011-05-24 06:44:29
【问题描述】:

我有一个类 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");
}

这是最好的选择吗?如果是这样,pre-0x 中最好的方法是什么?有没有办法摆脱构造函数中完全不必要的副本?不幸的是,移动临时的在这里不起作用(对吧?)。

【问题讨论】:

  • 好吧,unique_ptr 在所有实际用途中都表现为指针,区别在于生命周期管理。
  • +1 Extra parens to prevent MVP. :D
  • 顺便说一句。该构造函数模板可能会干扰您的复制构造函数,至少我似乎记得 Visual Studio 在某些时候抱怨过它......
  • 你能解释一下Extra parens to prevent MVP.吗?
  • @Mark MVP = most vexing parse

标签: c++ oop c++11


【解决方案1】:

没有虚拟克隆方法就无法克隆多态对象。所以你可以:

  • 传递并持有一个引用并确保打印机在构造信使的代码中的信使之前没有被破坏,
  • 传递并持有一个智能指针并使用 new 创建打印机实例,
  • 传递引用并使用克隆方法在堆上创建打印机实例或
  • 将对实际类型的引用传递给模板,并在您仍然知道类型的情况下使用 new 创建实例。

最后一个是你对 C++0x std::unique_ptr 的建议,但在这种情况下,C++03 std::auto_ptr 会为你提供完全相同的服务(即你不需要移动它,否则它们是一样)。

编辑:好的,嗯,还有一种方法:

  • 使printer 本身成为指向实际实现的智能指针。比它同时具有可复制性和多态性,但代价是一些复杂性。

【讨论】:

  • 哈,我完全忘记了auto_ptr
【解决方案2】:

将评论扩展为正确的答案...

这里主要关注的是所有权。从您的代码来看,messenger 的每个实例似乎都拥有自己的打印机实例 - 但实际上您正在传递一个预先构建的打印机(可能带有一些额外的状态),然后您需要将其复制到您自己的实例中的printer。鉴于对象printer 的隐含性质(即打印某些东西),我认为它正在打印的对象是共享资源 - 鉴于此,每个messenger 实例都没有任何意义它是 printer 的自己的副本(例如,如果您需要锁定访问 std::cout 怎么办)?

从设计的角度来看,messenger 在构造上需要的实际上是指向某个共享资源的指针 - 鉴于此,shared_ptr(更好的是,weak_ptr)是更好的选择。

现在,如果您不想使用weak_ptr,而宁愿存储引用,请考虑是否可以将messenger 耦合到printer 的类型,耦合留给用户,你不在乎 - 当然,这样做的主要缺点是 messenger 将无法被包含。注意:您可以指定一个可以输入messenger 的特征(或策略)类,这提供了打印机的类型信息(并且可以由用户控制)。

第三种选择是,如果您可以完全控制打印机集,在这种情况下持有一个变体类型 - 恕我直言,它更简洁,并且避免了多态性。

最后,如果你不能耦合,你不能控制打印机,并且你想要你自己的printer 实例(相同类型),转换构造函数模板是前进的方向,但是添加一个disable_if 来防止它被错误地调用(即作为正常的复制ctor)。

总而言之,我会将打印机视为共享资源并持有weak_ptr,因为坦率地说,它可以更好地控制该共享资源

【讨论】:

  • 不要太拘泥于示例标识符的语义,很难找到一个简单的示例来解释这个问题。但在我的实际用例中,该对象不是共享资源。
  • @Nim 哪个复制器要求?不,根本没有共享资源。
  • @Konrad,我的意思是,你为什么要复制构建?如果printer 的实例之间没有共享任何内容,您为什么还要费心调用copy-ctor?
  • @Nim 在哪里?在构建智能指针?我怎样才能做到这一点?
  • @Konrad,即使在开始时的简单示例中,您也在为打印机调用 copy-ctor(对我而言,这意味着您正在从传入的复制一些 state打印机到messenger 拥有的打印机实例) 至于智能指针的构造,你不能再做new TPrinter() 吗?除非我遗漏了一些非常明显的东西......(很可能是这种情况)
【解决方案3】:
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,但将 unique_ptr 替换为 auto_ptr 并放弃右值引用重载。

此外,如果你对一个有点狡猾的接口没问题,你可以考虑为 C++03 使用某种“虚拟”构造函数。

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;
};

或者您可以考虑 auto_ptr 在 C++03 中用于“移动”的相同策略。当然要谨慎使用,但完全合法且可行。这样做的问题是你正在影响所有printer 子类。

【讨论】:

    【解决方案4】:

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

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

    private:
        printer& pp;
    };
    

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

    【讨论】:

    • 一旦m 离开范围,这将导致一个悬空引用。至于为什么我不想使用智能指针——因为它会导致 API 不一致(用户什么时候需要使用对象,什么时候需要(智能)指针?)并且泄露了实现细节。
    • 您是否想要打印机对象的副本。什么都有道理。如果您不想要副本,那么除了引用和指针之外,您无能为力。我也不明白为什么使用智能指针会泄漏实现细节..
    • @gigantt.com 它泄露了实现细节,因为用户不关心类在内部对对象做了什么(甚至可能会发生变化)。它所关心的只是该类需要这样一个对象的实例。这是暂时的(因此只需要引用)还是存储的都无关紧要。但我开始意识到资源所有权会总是泄漏信息,无论如何。
    • 这个所谓的泄漏有什么意义?用户不知道该类是否实际上存储了一个指针。它所知道的只是它必须被赋予一个多态项,而在 C++ 中,这是通过 ptrs/refs 实现的……我看不到问题所在。
    • @gigantt 同样,用户并不关心指针或引用。用户甚至不关心多态性。在他看来,一个对象被传递到构造函数中。而且多态性也不需要指针,引用工作得很好——但前提是你不接管对象的所有权。
    【解决方案5】:

    当你有一把金锤时,一切看起来都像钉子

    嗯,我最新的金锤是类型擦除。说真的,我不会使用它,但话又说回来,我会传递一个指针并让调用者创建并注入依赖项。

    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类有自己的职责,类型擦除不是其中之一,可以委托。

    【讨论】:

      【解决方案6】:

      模板化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");
      

      假设您有其他数据类型然后pp 与模板类型无关,那么可以将它们移动到非模板基类并且该基类可以由messenger 继承。我没有详细描述,但我希望这一点应该更清楚。

      【讨论】:

      • 在我的上下文中不可能。实际上,如果这是可能的,那么整个问题就不会出现。我也不明白拥有signal static 的用处:如果我仍然使用模板,我将不再关心虚函数和动态调度。
      • @Konrad,拥有static signal() 不会生成signal 的许多副本,因为不会对template 参数有任何依赖。还要在signal() 中使用pp,你必须通过它。但是,由于无法使用template,因此这种方法没有用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多