【问题标题】:How to design a class where the user/caller has options to provide a custom behavior using a custom class如何设计一个类,其中用户/调用者可以选择使用自定义类提供自定义行为
【发布时间】:2011-01-16 00:32:39
【问题描述】:

我遇到一个问题,我想要一个类,它的行为可以由另一个类自定义,例如,Foo 的构造函数接受某种类型的类的参数:

class Bar { //The default class that define behavior
};

template <typename T = Bar>
class Foo {
  public:
    Foo(T* t = 0) t_(t) {
      if (t_ == 0) t_ = new T();
    }
    ~Foo() { 
      delete t_;
    }
}

现在如果有人在客户端代码中使用 Foo:

Foo foo;

一切都很好。但是,如果我们想提供自定义类:

class Bar1 { };

Foo<Bar1> foo(new Bar1()); // This is OK
Bar1 b;
Foo<Bar1> foo(&b); // Error, b is not dynamically allocated

我可以使用任何设计模式来防止这种错误吗?或者,是否有任何技术或语义可供 Foo 类的用户选择/指定谁拥有 bar 对象?所以例如上面的 Foo 析构函数可以是这样的:

    ~Foo() { 
      if (t_ is owned by this object) delete t_;
    }

Bar 或 Bar1 或任何在 Foo(T* t) 中作为 t 传递的类可能是一个大对象,所以如果可能的话,我宁愿不按值传递它。

更新: 我的想法是让用户能够执行以下操作:

Foo foo(new Bar(1, 2, etc..));
//or this:
Bar bar(1, 2, etc..);
Foo foo(bar);

但是如果 bar 是一个大对象(例如,包含一个数组),那么按值传递 bar 将是低效的。唯一的方法是让 bar 通过引用或指针传递,但我也希望用户能够将 bar 与参数化构造函数一起使用,因此我的困惑开始了。 关于自动变量范围,如果用户执行以下操作将是安全的:

int main() {
  Bar1 bar1(1,2,3);
  Foo foo(&bar1);
  return 0;
}

只要 Foo 不删除 bar1。

【问题讨论】:

  • 啊。这是我最喜欢的咆哮。
  • 自从我在 C++ 中工作已经有一段时间了,所以我可能是错的,但是如果 Bar1 在堆栈上分配并且比你的 Foo 实例更早超出范围,那么 Foo 将有对已被破坏的对象的引用。复制或使用智能指针的另一个原因如下面的答案所示。
  • @Martin:不,不一样,因为我不想覆盖 new 和 delete 运算符或类似的东西,也许标题有点误导。
  • @leiiv:还是一模一样。你想做的事情是 1) 不实用 2) 没用 3) 已经被问过一百万次了。

标签: c++


【解决方案1】:

您无法检测堆栈分配和堆分配之间的区别(标准甚至没有提到堆栈),但这不是这里的问题。

Foo 不应该删除不属于它的东西。如果它想要自己的副本,那么它应该只制作一个副本:

template <class T = Bar>
class Foo
{
  T t_;
public:
  Foo() {}
  Foo(const T& t) : t_(t) {}
};

或者如果你需要它是一个指针:

template <class T = Bar>
class Foo
{
  T* t_;
public:
  Foo() : t_(new T()) {}
  Foo(const T& t) : t_(new T(t)) {}
};

你不能随便删除别人给你的东西,不管你是否知道它是堆栈分配的还是堆分配的。如果你的Foodeletes 去了,但调用代码仍然想使用它怎么办?或者,如果将同一个对象传递给您的两个 Foo 对象并且它们都是 delete 呢?

您的选择是制作副本或不删除它。

复制的替代方法是标记您是使用自己的还是他人的:

template <class T = Bar>
class Foo
{
  T* t_;
  bool owned;
public:
  Foo() : t_(new T()), owned(true) {}
  Foo(T* t) : t_(t), owned(false) {}
  ~Foo() { if (owned) delete t_; }
};

如果您想知道,t 在我之前的解决方案中是由 const-ref 传递的,而不是按值传递的,因此不会产生过多的费用,尽管在复制过程中可能会产生费用。

【讨论】:

  • 可以,但问题仍然存在,调用代码不知道Foo 正在窃取其数据。
  • 其实我想要的是调用者将 T 对象的所有权交给 Foo 对象。标记所有权是一个好主意,但它增加了另一个额外的参数。也许我可以提供两个构造函数?富(T* t); // 这是动态分配的 Foo(T& t); // 这是自动的
  • 是否有任何东西阻止用户提供 std::auto_ptr(&bar) ,其中 bar 是一个自动对象?
  • @Poita:我确实希望 Foo 拥有自己的副本,因此这一行:if (t_ == 0) t_ = new T();。但我也想给 Foo 的用户提供自定义对象的可能性,该对象可能有一个带有参数/参数的构造函数。
【解决方案2】:

使用智能指针,您不必担心内存管理。如果你真的需要,我想你应该让你的类的用户处理他们传入的内存,你的类应该只负责删除它创建的对象。

您必须考虑是否真的需要使用指针。像这样一个简单的设计:

template <typename T = Bar>
class Foo1 {
  T t_;
  public:
    Foo1() {
    }
    Foo1(const T& t) : t_(t) {
    }
};

可能就是你所需要的。像这样使用它没有任何问题:

int main() {
  Bar1 bar1(1,2,3);
  Foo1 foo(bar1); // specify custom behavior
  Foo1 foooo; // get default behavior
  return 0;
}

如果你真的需要指针,Dan 使用 std::auto_ptr 提供了一个很好的答案。你可以保证你总是有一个像这样创建的对象:

template <typename T = Bar>
class Foo2 {
  std::auto_ptr<T> t_;
  public:
    Foo2() : t_(std::auto_ptr<T>(new T)) { }
    Foo2(std::auto_ptr<T> t) : t_(t) {
      if (t_.get() == 0) t_ = std::auto_ptr<T>(new T);
    }
};

那么你可以这样称呼它:

std::auto_ptr<Bar1> s(new Bar1());
Foo2<Bar1> foo(s);
Foo2<Bar1> foooo;

至于用户将堆栈对象的地址传递给智能指针,这确实是用户做错事的错。一旦 auto_ptr 尝试删除对象,程序就会崩溃。

【讨论】:

    【解决方案3】:

    简短的回答,不。

    对于要使用的任意类型 T,例如 Bar1,您必须简单地接受您的类的用户可能会编写会爆炸的代码。

    但是,如果您只在构造类时构造类型 T 的实例,那么除了获取指向实例的指针之外,您还可以在 Foo 的代码中使用 Create 函数构造 Bar1 的实例。但是你不能将参数传递给构造函数。

    class Bar
    {
      public: static Bar * Create()
    }
    
    template <typename T>
    class Foo
    {
      public: static Foo<T>* Create() { return new Foo<T>(T::Create()); } 
    }
    

    【讨论】:

      【解决方案4】:

      我现在可以为除了纯粹的乐趣以外的任何事情表示赞同,但这里有一个类可以测试它的分配位置:

      class AllocTester {
      public:
         bool on_stack() {
            AllocTester outer;
            return on_stack_check(&outer);
         }
      private:
         bool on_stack_check(AllocTester *outer) {
            AllocTester inner;
            return (outer < &inner) == (this < &inner);
         }
      };
      

      这不适用于多线程、静态变量或不寻常的架构。因此,如果您尝试将其用于任何“真实”编码,您只会破坏您的应用程序。仅将其视为好奇心,在现实生活中,您的类的调用者应该负责正确处理分配的内存。

      通常如果有一个分配内存的函数,应该有一个相应的函数再次释放它。如果该类拥有传递的指针的所有权,则应明确说明这一点。也许甚至使用auto_ptr 也是一个好主意,因为它明确地将指针的所有权转移给构造函数。

      【讨论】:

      • 我想知道的是用户是否可以明确地决定传递指针的所有权,而不是 Foo 类。
      【解决方案5】:

      Poita_answer 中澄清我的 cmets:

      template <typename T = Bar> 
      class Foo { 
          T& t_;
          std::auto_ptr<T> pT_; 
      
      public: 
          Foo(T& t) : t_(t) { 
          }
          Foo(std::auto_ptr<T> pT) : pT_(pT), t_(*pT) { 
          } 
      
          T& getT() const {
              T* pT = pT_.get();
              if (pT != NULL)
                  return *pT;
              return t_;
          }
      };
      

      然后您可以按如下方式使用它:

      std::auto_ptr<Bar> pBar(new Bar());
      Foo<Bar> foo0(pBar);
      Bar& b0 = foo0.getT();
      
      Bar b;
      Foo<Bar> foo1(b);
      Bar& b1 = foo1.getT();
      

      正如这段代码所示,您应该(几乎)总是将new 的结果放入管理内存的某个对象中。 std::auto_ptr&lt;&gt; 会将指针的所有权传递给 Foo。或者,您可以直接在构造函数中传递指针,但这更容易调用Foo(&amp;bar)

      【讨论】:

      • 传递引用的能力提出了一个很好的问题,你真的需要允许用户传递指针吗?如果你能侥幸逃脱,最好不要使用它们。
      • 是否可以为两个构造函数参数提供默认值?据我所知,如果您想在构造函数中使用默认值,那么将引用作为数据成员的问题很难处理。你知道什么技巧吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-01
      • 1970-01-01
      • 2011-06-15
      • 1970-01-01
      相关资源
      最近更新 更多